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.
- 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: []
|