Paletti 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/bin/pal +29 -0
  3. data/lib/paletti.rb +71 -0
  4. data/lib/paletti/pixel.rb +91 -0
  5. metadata +49 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9b4eb61346249f243156a63eb8a889be54807e27
4
+ data.tar.gz: fa3dce231ec9e9503f4245849bc65808d5fb0fda
5
+ SHA512:
6
+ metadata.gz: ff5b42b80f6fbb52ae401ff39a6ebad599725aa470bc6bc56ebedae3578a3cea7b24add8e09909a53a722a0cbd2299f6ce67cbd5cbd69638ef58e5fc49c61eaa
7
+ data.tar.gz: 51948fca59a4540d08b3a31fb37606de4286fed95bf9d0152a01d4aae04a75011d129893ef0dd84ef186d531ca24bdeeccdace130ade9d723314211abc7d063e
data/bin/pal ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'paletti'
4
+ require 'optparse'
5
+
6
+ begin
7
+ option_parser = OptionParser.new do |opts|
8
+ executable_name = File.basename($PROGRAM_NAME)
9
+ opts.banner = "
10
+ Paletti takes an image and finds its background color as well as the best primary, secondary, and detail text colors that are readable on the background color.
11
+
12
+ Usage: #{executable_name} image_path
13
+ "
14
+ end
15
+ end
16
+
17
+ if ARGV.empty?
18
+ puts "Error: you must specifiy a path (local or URL) to an image."
19
+ puts option_parser.help
20
+ else
21
+ img = Paletti.new(ARGV[0])
22
+ bg_pixel = img.background_pixel
23
+ text_pixels = img.text_pixels
24
+ puts "Background color:\nR: #{bg_pixel.to_norm_rgba[0].to_i} G: #{bg_pixel.to_norm_rgba[1].to_i} B: #{bg_pixel.to_norm_rgba[2].to_i}\n\n"
25
+ puts "Text colors:\n"
26
+ text_pixels.each do |pixel|
27
+ puts "R: #{pixel.to_norm_rgba[0].to_i} G: #{pixel.to_norm_rgba[1].to_i} B: #{pixel.to_norm_rgba[2].to_i}\n"
28
+ end
29
+ end
@@ -0,0 +1,71 @@
1
+ require 'rmagick'
2
+ require_relative 'paletti/pixel'
3
+
4
+ class Paletti
5
+
6
+ def initialize(path_to_image)
7
+ # Load the image at the given path
8
+ @image = Magick::Image.read(path_to_image)[0]
9
+ end
10
+
11
+ def background_pixel
12
+ if @background_pixel
13
+ return @background_pixel
14
+ end
15
+
16
+ # Make an array of all the edge/border pixels
17
+ border_pixels = []
18
+ @image.each_pixel do |pixel, col_idx, row_idx|
19
+ if col_idx == 0 || row_idx == 0 || col_idx == @image.columns - 1 || row_idx == @image.rows - 1
20
+ border_pixels.push(pixel)
21
+ end
22
+ end
23
+
24
+ # Make a hash of the edge/border pixel frequencies and sort by frequency
25
+ border_pixel_counts = Hash.new(0)
26
+ border_pixels.each { |border_pixel| border_pixel_counts[border_pixel] += 1 }
27
+ sorted_border_pixels = border_pixel_counts.sort_by { |pixel, count| -count }
28
+ sorted_border_pixels = sorted_border_pixels.flatten.select! do |pixel|
29
+ pixel.class == Magick::Pixel
30
+ end
31
+
32
+ # Get a non black or white pixel if possible
33
+ pixel = sorted_border_pixels.first
34
+ backup_pixel = pixel.dup
35
+ while !pixel.nil? && pixel.is_black_or_white? && sorted_border_pixels.length > 1
36
+ sorted_border_pixels.delete(pixel)
37
+ pixel = sorted_border_pixels.find { |p| border_pixel_counts[p].to_f / border_pixel_counts[pixel].to_f > 0.05 && !p.is_black_or_white? }
38
+ end
39
+ return @background_pixel = pixel || backup_pixel
40
+ end
41
+
42
+ def text_pixels
43
+ if @text_pixels
44
+ return @text_pixels
45
+ end
46
+
47
+ # Make an array of all the pixels and sort by frequency
48
+ pixels = []
49
+ @image.each_pixel { |pixel| pixels.push(pixel) }
50
+ # For speed, just use a random sample of 250,000 pixels max
51
+ pixels = pixels.sample(250_000) if pixels.length > 250_000
52
+ pixel_counts = Hash.new(0)
53
+ pixels.each do |pixel|
54
+ if pixel.to_hsla[1] < 0.15 * 255.to_f
55
+ pixel = Magick::Pixel.from_hsla(pixel.to_hsla[0], 0.15 * 255.to_f, pixel.to_hsla[2], pixel.to_hsla[3])
56
+ end
57
+ pixel_counts[pixel] += 1
58
+ end
59
+ sorted_pixels = pixel_counts.sort_by { |pixel, count| -count }
60
+ sorted_pixels = sorted_pixels.flatten.select! { |pixel| pixel.class == Magick::Pixel }
61
+
62
+ # Get the most common three colors that are distinct from each other and the background color
63
+ @text_pixels = []
64
+ while @text_pixels.length < 3
65
+ found = (sorted_pixels.find { |pixel| pixel.is_contrasting?(self.background_pixel) && @text_pixels.all? { |text_pixel| text_pixel.is_distinct?(pixel) } })
66
+ @text_pixels.push(found)
67
+ end
68
+ return @text_pixels
69
+ end
70
+
71
+ end
@@ -0,0 +1,91 @@
1
+ class Magick::Pixel
2
+
3
+ def to_norm_rgba
4
+ [self.red.to_f * NORM_FACTOR, self.green.to_f * NORM_FACTOR, self.blue.to_f * NORM_FACTOR, self.opacity.to_f * NORM_FACTOR]
5
+ end
6
+
7
+ def norm_red
8
+ self.red.to_f * NORM_FACTOR
9
+ end
10
+
11
+ def norm_green
12
+ self.green.to_f * NORM_FACTOR
13
+ end
14
+
15
+ def norm_blue
16
+ self.blue.to_f * NORM_FACTOR
17
+ end
18
+
19
+ def norm_opacity
20
+ self.opacity.to_f * NORM_FACTOR
21
+ end
22
+
23
+ def is_black_or_white?
24
+ r, g, b = self.norm_red, self.norm_green, self.norm_blue
25
+ black_thresh = 0.09 * 255.to_f
26
+ white_thresh = 0.91 * 255.to_f
27
+ (r > white_thresh && g > white_thresh && b > white_thresh) || (r < black_thresh && g < black_thresh && b < black_thresh) ? true : false
28
+ end
29
+
30
+ def is_distinct?(other_pixel)
31
+ r, g, b, a = self.norm_red, self.norm_green, self.norm_blue, self.norm_opacity
32
+ other_r, other_g, other_b, other_a = other_pixel.norm_red, other_pixel.norm_green, other_pixel.norm_blue, other_pixel.norm_opacity
33
+ upper_thresh = 0.25 * 255.to_f
34
+ lower_thresh = 0.03 * 255.to_f
35
+
36
+ if (r - other_r).abs > upper_thresh || (g - other_g).abs > upper_thresh || (b - other_b).abs > upper_thresh || (a - other_a).abs > upper_thresh
37
+ if (r - g).abs < lower_thresh && (r - b).abs < lower_thresh && (other_r - other_g).abs < lower_thresh && (other_r - other_b).abs < lower_thresh
38
+ return false
39
+ end
40
+ return true
41
+ end
42
+ return false
43
+ end
44
+
45
+ def is_contrasting?(other_pixel)
46
+ # Calculate contrast using the W3C formula
47
+ # http://www.w3.org/TR/WCAG20-TECHS/G145.html
48
+ lum = self.luminance
49
+ other_pixel_lum = other_pixel.luminance
50
+ contrast = 0
51
+ if lum > other_pixel_lum
52
+ contrast = (lum + 0.05) / (other_pixel_lum + 0.05)
53
+ else
54
+ contrast = (other_pixel_lum + 0.05) / (lum + 0.05)
55
+ end
56
+
57
+ return contrast.to_f > 3
58
+ end
59
+
60
+ def is_dark?
61
+ return self.luminance < 0.5
62
+ end
63
+
64
+ def luminance
65
+ r, g, b = self.norm_red / 255.to_f, self.norm_green / 255.to_f, self.norm_blue / 255.to_f
66
+ if r <= 0.03928
67
+ r = r / 12.92
68
+ else
69
+ r = ((r + 0.055) / 1.055) ** 2.4
70
+ end
71
+
72
+ if g <= 0.03928
73
+ g = g / 12.92
74
+ else
75
+ g = ((g + 0.055) / 1.055) ** 2.4
76
+ end
77
+
78
+ if b <= 0.03928
79
+ b = b / 12.92
80
+ else
81
+ b = ((b + 0.055) / 1.055) ** 2.4
82
+ end
83
+
84
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b
85
+ end
86
+
87
+ private
88
+
89
+ NORM_FACTOR = 255.to_f / Magick::QuantumRange.to_f
90
+
91
+ end
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: Paletti
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Satyam Ghodasara
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-11-24 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Paletti takes an image and finds its background color as well as the
14
+ best primary, secondary, and detail text colors that are readable on the background
15
+ color.
16
+ email: sghodas@gmail.com
17
+ executables:
18
+ - pal
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - bin/pal
23
+ - lib/paletti.rb
24
+ - lib/paletti/pixel.rb
25
+ homepage: https://github.com/sghodas/paletti
26
+ licenses:
27
+ - MIT
28
+ metadata: {}
29
+ post_install_message:
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ requirements: []
44
+ rubyforge_project:
45
+ rubygems_version: 2.4.5.1
46
+ signing_key:
47
+ specification_version: 4
48
+ summary: Paletti generates readable text colors to display over images.
49
+ test_files: []