Paletti 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/pal +29 -0
- data/lib/paletti.rb +71 -0
- data/lib/paletti/pixel.rb +91 -0
- metadata +49 -0
checksums.yaml
ADDED
@@ -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
|
data/lib/paletti.rb
ADDED
@@ -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: []
|