zpng 0.2.0 → 0.2.1
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.
- data/Gemfile +0 -1
- data/Gemfile.lock +0 -2
- data/README.md +50 -10
- data/README.md.tpl +15 -1
- data/TODO +6 -0
- data/VERSION +1 -1
- data/lib/zpng.rb +4 -32
- data/lib/zpng/adam7_decoder.rb +8 -1
- data/lib/zpng/chunk.rb +7 -24
- data/lib/zpng/cli.rb +196 -141
- data/lib/zpng/color.rb +85 -30
- data/lib/zpng/hexdump.rb +86 -0
- data/lib/zpng/image.rb +99 -21
- data/lib/zpng/metadata.rb +20 -0
- data/lib/zpng/pixels.rb +25 -0
- data/lib/zpng/scan_line.rb +139 -87
- data/lib/zpng/string_ext.rb +9 -1
- data/lib/zpng/text_chunk.rb +75 -0
- data/samples/itxt.png +0 -0
- data/spec/adam7_spec.rb +24 -0
- data/spec/alpha_spec.rb +28 -0
- data/spec/cli_spec.rb +62 -0
- data/spec/color_spec.rb +12 -2
- data/spec/deinterlace_spec.rb +19 -0
- data/spec/metadata_spec.rb +22 -0
- data/spec/pixel_access_spec.rb +16 -0
- data/spec/pixels_enumerator_spec.rb +34 -0
- data/spec/set_random_pixel_spec.rb +13 -0
- data/spec/spec_helper.rb +1 -22
- data/spec/support/png_suite.rb +43 -0
- data/zpng.gemspec +15 -5
- metadata +16 -19
data/lib/zpng/color.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
module ZPNG
|
2
2
|
class Color
|
3
|
-
attr_accessor :r, :g, :b
|
4
|
-
|
3
|
+
attr_accessor :r, :g, :b
|
4
|
+
attr_reader :a
|
5
|
+
attr_accessor :depth
|
5
6
|
|
6
7
|
include DeepCopyable
|
7
8
|
|
@@ -9,16 +10,18 @@ module ZPNG
|
|
9
10
|
h = a.last.is_a?(Hash) ? a.pop : {}
|
10
11
|
@r,@g,@b,@a = *a
|
11
12
|
|
12
|
-
# default ALPHA = 0xff - opaque
|
13
|
-
@a ||= h[:alpha] || 0xff
|
14
|
-
|
15
13
|
# default sample depth for r,g,b and alpha = 8 bits
|
16
14
|
@depth = h[:depth] || 8
|
17
|
-
|
15
|
+
|
16
|
+
# default ALPHA = 0xff - opaque
|
17
|
+
@a ||= h[:alpha] || (2**@depth-1)
|
18
18
|
end
|
19
19
|
|
20
|
-
|
21
|
-
|
20
|
+
def a= a
|
21
|
+
@a = a || (2**@depth-1) # NULL alpha means fully opaque
|
22
|
+
end
|
23
|
+
alias :alpha :a
|
24
|
+
alias :alpha= :a=
|
22
25
|
|
23
26
|
BLACK = Color.new(0 , 0, 0)
|
24
27
|
WHITE = Color.new(255,255,255)
|
@@ -32,6 +35,8 @@ module ZPNG
|
|
32
35
|
PURPLE= MAGENTA =
|
33
36
|
Color.new(255, 0,255)
|
34
37
|
|
38
|
+
TRANSPARENT = Color.new(0,0,0,0)
|
39
|
+
|
35
40
|
ANSI_COLORS = [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white]
|
36
41
|
|
37
42
|
#ASCII_MAP = %q_ .`,-:;~"!<+*^(LJ=?vctsxj12FuoCeyPSah5wVmXA4G9$OR0MQNW#&%@_
|
@@ -51,6 +56,7 @@ module ZPNG
|
|
51
56
|
|
52
57
|
# euclidian distance - http://en.wikipedia.org/wiki/Euclidean_distance
|
53
58
|
def euclidian other_color
|
59
|
+
# TODO: different depths
|
54
60
|
r = (self.r.to_i - other_color.r.to_i)**2
|
55
61
|
r += (self.g.to_i - other_color.g.to_i)**2
|
56
62
|
r += (self.b.to_i - other_color.b.to_i)**2
|
@@ -58,7 +64,8 @@ module ZPNG
|
|
58
64
|
end
|
59
65
|
|
60
66
|
def white?
|
61
|
-
|
67
|
+
max = 2**depth-1
|
68
|
+
r == max && g == max && b == max
|
62
69
|
end
|
63
70
|
|
64
71
|
def black?
|
@@ -69,26 +76,51 @@ module ZPNG
|
|
69
76
|
a == 0
|
70
77
|
end
|
71
78
|
|
79
|
+
def opaque?
|
80
|
+
a.nil? || a == 2**depth-1
|
81
|
+
end
|
82
|
+
|
72
83
|
def to_grayscale
|
73
84
|
(r+g+b)/3
|
74
85
|
end
|
75
86
|
|
76
|
-
def
|
77
|
-
|
87
|
+
def to_gray_alpha
|
88
|
+
[to_grayscale, alpha]
|
89
|
+
end
|
90
|
+
|
91
|
+
# from_grayscale level
|
92
|
+
# from_grayscale level, :depth => 16
|
93
|
+
# from_grayscale level, alpha
|
94
|
+
# from_grayscale level, alpha, :depth => 16
|
95
|
+
def self.from_grayscale value, *args
|
96
|
+
Color.new value,value,value, *args
|
97
|
+
end
|
98
|
+
|
99
|
+
########################################################
|
100
|
+
# simple conversions
|
101
|
+
|
102
|
+
def to_i
|
103
|
+
((a||0) << 24) + ((r||0) << 16) + ((g||0) << 8) + (b||0)
|
78
104
|
end
|
79
105
|
|
80
106
|
def to_s
|
81
107
|
"%02X%02X%02X" % [r,g,b]
|
82
108
|
end
|
83
109
|
|
110
|
+
def to_a
|
111
|
+
[r, g, b, a]
|
112
|
+
end
|
113
|
+
|
84
114
|
########################################################
|
115
|
+
# complex conversions
|
85
116
|
|
86
|
-
# try to convert to pseudographics
|
117
|
+
# try to convert to one pseudographics ASCII character
|
87
118
|
def to_ascii map=ASCII_MAP
|
88
119
|
#p self
|
89
120
|
map[self.to_grayscale*(map.size-1)/(2**@depth-1), 1]
|
90
121
|
end
|
91
122
|
|
123
|
+
# convert to ANSI color name
|
92
124
|
def to_ansi
|
93
125
|
return to_depth(8).to_ansi if depth != 8
|
94
126
|
a = ANSI_COLORS.map{|c| self.class.const_get(c.to_s.upcase) }
|
@@ -96,6 +128,7 @@ module ZPNG
|
|
96
128
|
ANSI_COLORS[a.index(a.min)]
|
97
129
|
end
|
98
130
|
|
131
|
+
# HTML/CSS color in notation like #33aa88
|
99
132
|
def to_css
|
100
133
|
return to_depth(8).to_css if depth != 8
|
101
134
|
"#%02X%02X%02X" % [r,g,b]
|
@@ -104,30 +137,22 @@ module ZPNG
|
|
104
137
|
|
105
138
|
########################################################
|
106
139
|
|
107
|
-
def to_i
|
108
|
-
((a||0) << 24) + ((r||0) << 16) + ((g||0) << 8) + (b||0)
|
109
|
-
end
|
110
|
-
|
111
140
|
# change bit depth, return new Color
|
112
141
|
def to_depth new_depth
|
113
|
-
|
142
|
+
return self if depth == new_depth
|
143
|
+
|
144
|
+
color = Color.new :depth => new_depth
|
114
145
|
if new_depth > self.depth
|
115
|
-
%w'r g b'.each do |part|
|
116
|
-
color
|
117
|
-
if color%2 == 0
|
118
|
-
color <<= (new_depth-self.depth)
|
119
|
-
else
|
120
|
-
(new_depth-self.depth).times{ color = color*2 + 1 }
|
121
|
-
end
|
122
|
-
c.send("#{part}=", color)
|
146
|
+
%w'r g b a'.each do |part|
|
147
|
+
color.send("#{part}=", (2**new_depth-1)/(2**depth-1)*self.send(part))
|
123
148
|
end
|
124
149
|
else
|
125
150
|
# new_depth < self.depth
|
126
|
-
%w'r g b'.each do |part|
|
127
|
-
|
151
|
+
%w'r g b a'.each do |part|
|
152
|
+
color.send("#{part}=", self.send(part)>>(self.depth-new_depth))
|
128
153
|
end
|
129
154
|
end
|
130
|
-
|
155
|
+
color
|
131
156
|
end
|
132
157
|
|
133
158
|
def inspect
|
@@ -136,16 +161,46 @@ module ZPNG
|
|
136
161
|
s << " r=" + (r ? "%04x" % r : "????")
|
137
162
|
s << " g=" + (g ? "%04x" % g : "????")
|
138
163
|
s << " b=" + (b ? "%04x" % b : "????")
|
164
|
+
s << " alpha=%04x" % alpha if alpha && alpha != 0xffff
|
139
165
|
else
|
140
166
|
s << " #"
|
141
167
|
s << (r ? "%02x" % r : "??")
|
142
168
|
s << (g ? "%02x" % g : "??")
|
143
169
|
s << (b ? "%02x" % b : "??")
|
170
|
+
s << " alpha=%02x" % alpha if alpha && alpha != 0xff
|
144
171
|
end
|
145
|
-
s << " a=#{a}" if a && alpha_depth != 0
|
146
172
|
s << " depth=#{depth}" if depth != 8
|
147
|
-
s << " alpha_depth=#{alpha_depth}" if alpha_depth != 8 && alpha_depth != 0
|
148
173
|
s << ">"
|
149
174
|
end
|
175
|
+
|
176
|
+
# compare with other color
|
177
|
+
def == c
|
178
|
+
return false unless c.is_a?(Color)
|
179
|
+
c1,c2 =
|
180
|
+
if self.depth > c.depth
|
181
|
+
[self, c.to_depth(self.depth)]
|
182
|
+
else
|
183
|
+
[self.to_depth(c.depth), c]
|
184
|
+
end
|
185
|
+
c1.r == c2.r && c1.g == c2.g && c1.b == c2.b && c1.a == c2.a
|
186
|
+
end
|
187
|
+
alias :eql? :==
|
188
|
+
|
189
|
+
# compare with other color
|
190
|
+
def <=> c
|
191
|
+
c1,c2 =
|
192
|
+
if self.depth > c.depth
|
193
|
+
[self, c.to_depth(self.depth)]
|
194
|
+
else
|
195
|
+
[self.to_depth(c.depth), c]
|
196
|
+
end
|
197
|
+
r = c1.to_grayscale <=> c2.to_grayscale
|
198
|
+
r == 0 ? (c1.to_a <=> c2.to_a) : r
|
199
|
+
end
|
200
|
+
|
201
|
+
# for Array.uniq()
|
202
|
+
def hash
|
203
|
+
self.to_i
|
204
|
+
end
|
150
205
|
end
|
151
206
|
end
|
data/lib/zpng/hexdump.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module ZPNG
|
4
|
+
module Hexdump
|
5
|
+
|
6
|
+
def hexdump *args, &block
|
7
|
+
print Hexdump.dump(*args, &block)
|
8
|
+
end
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def dump data, h = {}
|
12
|
+
offset = h[:offset] || 0
|
13
|
+
add = h[:add] || 0
|
14
|
+
size = h[:size] || (data.size-offset)
|
15
|
+
tail = h[:tail] || "\n"
|
16
|
+
width = h[:width] || 0x10 # row width, in bytes
|
17
|
+
|
18
|
+
h[:show_offset] = true unless h.key?(:show_offset)
|
19
|
+
h[:dedup] = true unless h.key?(:dedup)
|
20
|
+
|
21
|
+
size = data.size-offset if size+offset > data.size
|
22
|
+
|
23
|
+
r = ''; prevhex = ''; c = nil; prevdup = false
|
24
|
+
while true
|
25
|
+
ascii = ''; hex = ''
|
26
|
+
width.times do |i|
|
27
|
+
hex << ' ' if i%8==0 && i>0
|
28
|
+
if c = ((size > 0) && data[offset+i])
|
29
|
+
hex << "%02x " % c.ord
|
30
|
+
ascii << ((32..126).include?(c.ord) ? c : '.')
|
31
|
+
else
|
32
|
+
hex << ' '
|
33
|
+
ascii << ' '
|
34
|
+
end
|
35
|
+
size-=1
|
36
|
+
end
|
37
|
+
|
38
|
+
if h[:dedup] && hex == prevhex
|
39
|
+
row = "*"
|
40
|
+
yield(row, offset+add, ascii) if block_given?
|
41
|
+
r << row << "\n" unless prevdup
|
42
|
+
prevdup = true
|
43
|
+
else
|
44
|
+
row = (h[:show_offset] ? ("%08x: " % (offset + add)) : '') + hex + ' |' + ascii + "|"
|
45
|
+
yield(row, offset+add, ascii) if block_given?
|
46
|
+
r << row << "\n"
|
47
|
+
prevdup = false
|
48
|
+
end
|
49
|
+
|
50
|
+
offset += width
|
51
|
+
prevhex = hex
|
52
|
+
break if size <= 0
|
53
|
+
end
|
54
|
+
if h[:show_offset] && prevdup
|
55
|
+
row = "%08x: " % (offset + add)
|
56
|
+
yield(row) if block_given?
|
57
|
+
r << row
|
58
|
+
end
|
59
|
+
r.chomp + tail
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
if $0 == __FILE__
|
66
|
+
h = {}
|
67
|
+
case ARGV.size
|
68
|
+
when 0
|
69
|
+
puts "gimme fname [offset] [size]"
|
70
|
+
exit
|
71
|
+
when 1
|
72
|
+
fname = ARGV[0]
|
73
|
+
when 2
|
74
|
+
fname = ARGV[0]
|
75
|
+
h[:offset] = ARGV[1].to_i
|
76
|
+
when 3
|
77
|
+
fname = ARGV[0]
|
78
|
+
h[:offset] = ARGV[1].to_i
|
79
|
+
h[:size] = ARGV[2].to_i
|
80
|
+
end
|
81
|
+
File.open(fname,"rb") do |f|
|
82
|
+
f.seek h[:offset] if h[:offset]
|
83
|
+
@data = f.read(h[:size])
|
84
|
+
end
|
85
|
+
puts ZPNG::Hexdump.dump(@data)
|
86
|
+
end
|
data/lib/zpng/image.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module ZPNG
|
2
2
|
class Image
|
3
|
-
attr_accessor :data, :header, :chunks, :scanlines, :imagedata
|
3
|
+
attr_accessor :data, :header, :chunks, :scanlines, :imagedata
|
4
4
|
alias :hdr :header
|
5
5
|
|
6
6
|
include DeepCopyable
|
@@ -28,13 +28,16 @@ module ZPNG
|
|
28
28
|
@adam7 ||= Adam7Decoder.new(self)
|
29
29
|
end
|
30
30
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
31
|
+
class << self
|
32
|
+
# load image from file
|
33
|
+
def load fname
|
34
|
+
open(fname,"rb") do |f|
|
35
|
+
self.new(f)
|
36
|
+
end
|
35
37
|
end
|
38
|
+
alias :load_file :load
|
39
|
+
alias :from_file :load # as in ChunkyPNG
|
36
40
|
end
|
37
|
-
alias :load_file :load
|
38
41
|
|
39
42
|
# save image to file
|
40
43
|
def save fname
|
@@ -52,10 +55,19 @@ module ZPNG
|
|
52
55
|
|
53
56
|
def _from_hash h
|
54
57
|
@new_image = true
|
55
|
-
@chunks << (@header
|
58
|
+
@chunks << (@header = Chunk::IHDR.new(h))
|
56
59
|
if @header.palette_used?
|
57
|
-
|
58
|
-
|
60
|
+
if h.key?(:palette)
|
61
|
+
if h[:palette]
|
62
|
+
@chunks << h[:palette]
|
63
|
+
else
|
64
|
+
# :palette => nil
|
65
|
+
# assume palette will be added later
|
66
|
+
end
|
67
|
+
else
|
68
|
+
@chunks << Chunk::PLTE.new
|
69
|
+
palette[0] = h[:background] || h[:bg] || Color::BLACK # add default bg color
|
70
|
+
end
|
59
71
|
end
|
60
72
|
end
|
61
73
|
|
@@ -86,8 +98,6 @@ module ZPNG
|
|
86
98
|
case chunk
|
87
99
|
when Chunk::IHDR
|
88
100
|
@header = chunk
|
89
|
-
when Chunk::PLTE
|
90
|
-
@palette = chunk
|
91
101
|
when Chunk::IEND
|
92
102
|
break
|
93
103
|
end
|
@@ -100,12 +110,58 @@ module ZPNG
|
|
100
110
|
end
|
101
111
|
|
102
112
|
public
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
113
|
+
|
114
|
+
# internal helper method for color types 0 (grayscale) and 2 (truecolor)
|
115
|
+
def _alpha_color color
|
116
|
+
return nil unless trns
|
117
|
+
|
118
|
+
# For color type 0 (grayscale), the tRNS chunk contains a single gray level value, stored in the format:
|
119
|
+
#
|
120
|
+
# Gray: 2 bytes, range 0 .. (2^bitdepth)-1
|
121
|
+
#
|
122
|
+
# For color type 2 (truecolor), the tRNS chunk contains a single RGB color value, stored in the format:
|
123
|
+
#
|
124
|
+
# Red: 2 bytes, range 0 .. (2^bitdepth)-1
|
125
|
+
# Green: 2 bytes, range 0 .. (2^bitdepth)-1
|
126
|
+
# Blue: 2 bytes, range 0 .. (2^bitdepth)-1
|
127
|
+
#
|
128
|
+
# (If the image bit depth is less than 16, the least significant bits are used and the others are 0)
|
129
|
+
# Pixels of the specified gray level are to be treated as transparent (equivalent to alpha value 0);
|
130
|
+
# all other pixels are to be treated as fully opaque ( alpha = (2^bitdepth)-1 )
|
131
|
+
|
132
|
+
@alpha_color ||=
|
133
|
+
case hdr.color
|
134
|
+
when COLOR_GRAYSCALE
|
135
|
+
v = trns.data.unpack('n')[0] & (2**hdr.depth-1)
|
136
|
+
Color.from_grayscale(v, :depth => hdr.depth)
|
137
|
+
when COLOR_RGB
|
138
|
+
a = trns.data.unpack('n3').map{ |v| v & (2**hdr.depth-1) }
|
139
|
+
Color.new(*a, :depth => hdr.depth)
|
140
|
+
else
|
141
|
+
raise "color2alpha only intended for GRAYSCALE & RGB color modes"
|
142
|
+
end
|
143
|
+
|
144
|
+
color == @alpha_color ? 0 : (2**hdr.depth-1)
|
145
|
+
end
|
146
|
+
|
147
|
+
public
|
148
|
+
|
149
|
+
###########################################################################
|
150
|
+
# chunks access
|
151
|
+
|
152
|
+
def trns
|
153
|
+
# not used "@trns ||= ..." here b/c it will call find() each time of there's no TRNS chunk
|
154
|
+
defined?(@trns) ? @trns : (@trns=@chunks.find{ |c| c.is_a?(Chunk::TRNS) })
|
107
155
|
end
|
108
156
|
|
157
|
+
def plte
|
158
|
+
@plte ||= @chunks.find{ |c| c.is_a?(Chunk::PLTE) }
|
159
|
+
end
|
160
|
+
alias :palette :plte
|
161
|
+
|
162
|
+
###########################################################################
|
163
|
+
# image attributes
|
164
|
+
|
109
165
|
def bpp
|
110
166
|
@header && @header.bpp
|
111
167
|
end
|
@@ -139,6 +195,10 @@ module ZPNG
|
|
139
195
|
end
|
140
196
|
end
|
141
197
|
|
198
|
+
def metadata
|
199
|
+
@metadata ||= Metadata.new(self)
|
200
|
+
end
|
201
|
+
|
142
202
|
def [] x, y
|
143
203
|
x,y = adam7.convert_coords(x,y) if interlaced?
|
144
204
|
scanlines[y][x]
|
@@ -153,7 +213,7 @@ module ZPNG
|
|
153
213
|
# we must decode all scanlines before doing any modifications
|
154
214
|
# or scanlines decoded AFTER modification of UPPER ones will be decoded wrong
|
155
215
|
def decode_all_scanlines
|
156
|
-
return if @all_scanlines_decoded
|
216
|
+
return if @all_scanlines_decoded || new_image?
|
157
217
|
@all_scanlines_decoded = true
|
158
218
|
scanlines.each(&:decode!)
|
159
219
|
end
|
@@ -201,7 +261,8 @@ module ZPNG
|
|
201
261
|
end
|
202
262
|
|
203
263
|
def export
|
204
|
-
|
264
|
+
# fill @imagedata, if not already filled
|
265
|
+
imagedata unless new_image?
|
205
266
|
|
206
267
|
# delete old IDAT chunks
|
207
268
|
@chunks.delete_if{ |c| c.is_a?(Chunk::IDAT) }
|
@@ -255,6 +316,16 @@ module ZPNG
|
|
255
316
|
deep_copy.crop!(params)
|
256
317
|
end
|
257
318
|
|
319
|
+
def pixels
|
320
|
+
Pixels.new(self)
|
321
|
+
end
|
322
|
+
|
323
|
+
def == other_image
|
324
|
+
width == other_image.width &&
|
325
|
+
height == other_image.height &&
|
326
|
+
pixels == other_image.pixels
|
327
|
+
end
|
328
|
+
|
258
329
|
def each_pixel &block
|
259
330
|
height.times do |y|
|
260
331
|
width.times do |x|
|
@@ -267,22 +338,29 @@ module ZPNG
|
|
267
338
|
# OR returns self if no need to deinterlace
|
268
339
|
def deinterlace
|
269
340
|
return self unless interlaced?
|
270
|
-
require 'pp'
|
271
|
-
pp chunks
|
272
341
|
|
273
342
|
# copy all but 'interlace' header params
|
274
343
|
h = Hash[*%w'width height depth color compression filter'.map{ |k| [k.to_sym, hdr.send(k)] }.flatten]
|
275
|
-
|
344
|
+
|
345
|
+
# don't auto-add palette chunk
|
346
|
+
h[:palette] = nil
|
347
|
+
|
348
|
+
# create new img
|
349
|
+
new_img = self.class.new h
|
350
|
+
|
351
|
+
# copy all but hdr/imagedata/end chunks
|
276
352
|
chunks.each do |chunk|
|
277
353
|
next if chunk.is_a?(Chunk::IHDR)
|
278
354
|
next if chunk.is_a?(Chunk::IDAT)
|
279
355
|
next if chunk.is_a?(Chunk::IEND)
|
280
356
|
new_img.chunks << chunk.deep_copy
|
281
357
|
end
|
358
|
+
|
359
|
+
# pixel-by-pixel copy
|
282
360
|
each_pixel do |c,x,y|
|
283
361
|
new_img[x,y] = c
|
284
362
|
end
|
285
|
-
|
363
|
+
|
286
364
|
new_img
|
287
365
|
end
|
288
366
|
end
|