zpng 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,20 @@
|
|
1
|
+
module ZPNG
|
2
|
+
class Metadata < Array
|
3
|
+
def initialize img = nil
|
4
|
+
return unless img
|
5
|
+
img.chunks.each do |c|
|
6
|
+
next unless c.is_a?(TextChunk)
|
7
|
+
self << [c.keyword, c.text, c.to_hash]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def [] *args
|
12
|
+
if args.first.is_a?(String)
|
13
|
+
each{ |a| return a[1] if a[0] == args.first }
|
14
|
+
nil
|
15
|
+
else
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/zpng/pixels.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module ZPNG
|
2
|
+
class Pixels
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
def initialize image
|
6
|
+
@image = image
|
7
|
+
end
|
8
|
+
|
9
|
+
def each
|
10
|
+
@image.height.times do |y|
|
11
|
+
@image.width.times do |x|
|
12
|
+
yield @image[x,y]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def == other
|
18
|
+
self.to_a == other.to_a
|
19
|
+
end
|
20
|
+
|
21
|
+
def uniq
|
22
|
+
self.to_a.uniq
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/zpng/scan_line.rb
CHANGED
@@ -8,6 +8,7 @@ module ZPNG
|
|
8
8
|
FILTER_PAETH = 4
|
9
9
|
|
10
10
|
attr_accessor :image, :idx, :filter, :offset, :bpp
|
11
|
+
attr_writer :decoded_bytes
|
11
12
|
|
12
13
|
def initialize image, idx
|
13
14
|
@image,@idx = image,idx
|
@@ -77,49 +78,63 @@ module ZPNG
|
|
77
78
|
decode_pixel(x)
|
78
79
|
end
|
79
80
|
|
80
|
-
def []= x,
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
color_idx = newcolor.to_grayscale
|
86
|
-
end
|
81
|
+
def []= x, color
|
82
|
+
case image.hdr.color
|
83
|
+
when COLOR_INDEXED # ALLOWED_DEPTHS: 1, 2, 4, 8
|
84
|
+
color_idx = image.palette.find_or_add(color)
|
85
|
+
raise "no color #{color.inspect} in palette" unless color_idx
|
87
86
|
|
88
|
-
case @bpp
|
89
|
-
when 1,2,4
|
90
|
-
pos = x*@bpp/8
|
91
|
-
b = decoded_bytes[pos].ord
|
92
87
|
mask = 2**@bpp-1
|
93
88
|
shift = 8-(x%(8/@bpp)+1)*@bpp
|
94
89
|
raise "invalid shift #{shift}" if shift < 0 || shift > 7
|
95
90
|
|
96
|
-
|
97
|
-
|
91
|
+
pos = x*@bpp/8
|
92
|
+
b = decoded_bytes[pos].ord
|
98
93
|
b = (b & (0xff-(mask<<shift))) | ((color_idx & mask) << shift)
|
99
94
|
decoded_bytes[pos] = b.chr
|
95
|
+
# TODO: transparency in TRNS
|
100
96
|
|
101
|
-
when 8
|
102
|
-
|
103
|
-
|
97
|
+
when COLOR_GRAYSCALE # ALLOWED_DEPTHS: 1, 2, 4, 8, 16
|
98
|
+
raw = color.to_depth(@bpp).to_grayscale
|
99
|
+
pos = x*@bpp/8
|
100
|
+
if @bpp == 16
|
101
|
+
decoded_bytes[pos,2] = [raw].pack('n')
|
104
102
|
else
|
105
|
-
|
103
|
+
mask = 2**@bpp-1
|
104
|
+
shift = 8-(x%(8/@bpp)+1)*@bpp
|
105
|
+
raise "invalid shift #{shift}" if shift < 0 || shift > 7
|
106
|
+
b = decoded_bytes[pos].ord
|
107
|
+
b = (b & (0xff-(mask<<shift))) | ((raw & mask) << shift)
|
108
|
+
decoded_bytes[pos] = b.chr
|
106
109
|
end
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
else
|
115
|
-
raise "unexpected colormode #{image.hdr.inspect}"
|
110
|
+
# TODO: transparency in TRNS
|
111
|
+
|
112
|
+
when COLOR_RGB # ALLOWED_DEPTHS: 8, 16
|
113
|
+
case @bpp
|
114
|
+
when 24; decoded_bytes[x*3,3] = color.to_depth(8).to_a.pack('C3')
|
115
|
+
when 48; decoded_bytes[x*6,6] = color.to_depth(16).to_a.pack('n3')
|
116
|
+
else raise "unexpected bpp #@bpp"
|
116
117
|
end
|
117
|
-
|
118
|
-
|
119
|
-
when
|
120
|
-
|
121
|
-
|
122
|
-
|
118
|
+
# TODO: transparency in TRNS
|
119
|
+
|
120
|
+
when COLOR_GRAY_ALPHA # ALLOWED_DEPTHS: 8, 16
|
121
|
+
case @bpp
|
122
|
+
when 16; decoded_bytes[x*2,2] = color.to_depth(8).to_gray_alpha.pack('C2')
|
123
|
+
when 32; decoded_bytes[x*4,4] = color.to_depth(16).to_gray_alpha.pack('n2')
|
124
|
+
else raise "unexpected bpp #@bpp"
|
125
|
+
end
|
126
|
+
|
127
|
+
when COLOR_RGBA # ALLOWED_DEPTHS: 8, 16
|
128
|
+
case @bpp
|
129
|
+
when 32; decoded_bytes[x*4,4] = color.to_depth(8).to_a.pack('C4')
|
130
|
+
when 64; decoded_bytes[x*8,8] = color.to_depth(16).to_a.pack('n4')
|
131
|
+
else raise "unexpected bpp #@bpp"
|
132
|
+
end
|
133
|
+
|
134
|
+
else
|
135
|
+
raise "unexpected color mode #{image.hdr.color}"
|
136
|
+
|
137
|
+
end # case image.hdr.color
|
123
138
|
end
|
124
139
|
|
125
140
|
def decode_pixel x
|
@@ -132,56 +147,82 @@ module ZPNG
|
|
132
147
|
decoded_bytes[x*@bpp/8]
|
133
148
|
end
|
134
149
|
|
135
|
-
color
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
if image.grayscale? && image.alpha_used?
|
156
|
-
# 16-bit grayscale + 16-bit alpha
|
157
|
-
raw.unpack 'n2'
|
158
|
-
else
|
159
|
-
# RGBA
|
160
|
-
return Color.new(*raw.unpack('C4'))
|
150
|
+
case image.hdr.color
|
151
|
+
when COLOR_INDEXED # ALLOWED_DEPTHS: 1, 2, 4, 8
|
152
|
+
mask = 2**@bpp-1
|
153
|
+
shift = 8-(x%(8/@bpp)+1)*@bpp
|
154
|
+
raise "invalid shift #{shift}" if shift < 0 || shift > 7
|
155
|
+
idx = (raw.ord >> shift) & mask
|
156
|
+
if image.trns
|
157
|
+
# transparency from tRNS chunk
|
158
|
+
# For color type 3 (indexed color), the tRNS chunk contains a series of one-byte alpha values,
|
159
|
+
# corresponding to entries in the PLTE chunk:
|
160
|
+
#
|
161
|
+
# Alpha for palette index 0: 1 byte
|
162
|
+
# Alpha for palette index 1: 1 byte
|
163
|
+
# ...
|
164
|
+
#
|
165
|
+
color = image.palette[idx].dup
|
166
|
+
if color.alpha = image.trns.data[idx]
|
167
|
+
# if it's not NULL - convert it from char to int,
|
168
|
+
# otherwise it means fully opaque color, as well as NULL alpha in ZPNG::Color
|
169
|
+
color.alpha = color.alpha.ord
|
161
170
|
end
|
162
|
-
|
163
|
-
# RGB 16 bits per sample
|
164
|
-
return Color.new(*raw.unpack('n3'), :depth => 16)
|
165
|
-
when 64
|
166
|
-
# RGB 16 bits per sample + 16-bit alpha
|
167
|
-
return Color.new(*raw.unpack('n4'), :depth => 16, :alpha_depth => 16)
|
171
|
+
return color
|
168
172
|
else
|
169
|
-
|
173
|
+
# no transparency
|
174
|
+
return image.palette[idx]
|
175
|
+
end
|
176
|
+
|
177
|
+
when COLOR_GRAYSCALE # ALLOWED_DEPTHS: 1, 2, 4, 8, 16
|
178
|
+
c = if @bpp == 16
|
179
|
+
raw.unpack('n')[0]
|
180
|
+
else
|
181
|
+
mask = 2**@bpp-1
|
182
|
+
shift = 8-(x%(8/@bpp)+1)*@bpp
|
183
|
+
raise "invalid shift #{shift}" if shift < 0 || shift > 7
|
184
|
+
(raw.ord >> shift) & mask
|
185
|
+
end
|
186
|
+
|
187
|
+
color = Color.from_grayscale(c, :depth => @bpp) # only in this color mode depth == bpp
|
188
|
+
color.alpha = image._alpha_color(color)
|
189
|
+
return color
|
190
|
+
|
191
|
+
when COLOR_RGB # ALLOWED_DEPTHS: 8, 16
|
192
|
+
color =
|
193
|
+
case @bpp
|
194
|
+
when 24 # RGB 8 bits per sample = 24bpp
|
195
|
+
Color.new(*raw.unpack('C3'))
|
196
|
+
when 48 # RGB 16 bits per sample = 48bpp
|
197
|
+
Color.new(*raw.unpack('n3'), :depth => 16)
|
198
|
+
else raise "COLOR_RGB unexpected bpp #@bpp"
|
199
|
+
end
|
200
|
+
|
201
|
+
color.alpha = image._alpha_color(color)
|
202
|
+
return color
|
203
|
+
|
204
|
+
when COLOR_GRAY_ALPHA # ALLOWED_DEPTHS: 8, 16
|
205
|
+
case @bpp
|
206
|
+
when 16 # 8-bit grayscale + 8-bit alpha
|
207
|
+
return Color.from_grayscale(*raw.unpack('C2'))
|
208
|
+
when 32 # 16-bit grayscale + 16-bit alpha
|
209
|
+
return Color.from_grayscale(*raw.unpack('n2'), :depth => 16)
|
210
|
+
else raise "COLOR_GRAY_ALPHA unexpected bpp #@bpp"
|
211
|
+
end
|
212
|
+
|
213
|
+
when COLOR_RGBA # ALLOWED_DEPTHS: 8, 16
|
214
|
+
case @bpp
|
215
|
+
when 32 # RGBA 8-bit/sample
|
216
|
+
return Color.new(*raw.unpack('C4'))
|
217
|
+
when 64 # RGBA 16-bit/sample
|
218
|
+
return Color.new(*raw.unpack('n4'), :depth => 16 )
|
219
|
+
else raise "COLOR_RGBA unexpected bpp #@bpp"
|
170
220
|
end
|
171
221
|
|
172
|
-
if image.grayscale?
|
173
|
-
Color.from_grayscale(color,
|
174
|
-
:alpha => alpha,
|
175
|
-
:depth => image.hdr.depth,
|
176
|
-
:alpha_depth => image.alpha_used? ? image.hdr.depth : 0
|
177
|
-
)
|
178
|
-
elsif image.palette
|
179
|
-
color = image.palette[color]
|
180
|
-
color.alpha = alpha
|
181
|
-
color
|
182
222
|
else
|
183
|
-
raise "
|
184
|
-
|
223
|
+
raise "unexpected color mode #{image.hdr.color}"
|
224
|
+
|
225
|
+
end # case img.hdr.color
|
185
226
|
end
|
186
227
|
|
187
228
|
def decoded_bytes
|
@@ -211,6 +252,19 @@ module ZPNG
|
|
211
252
|
end
|
212
253
|
|
213
254
|
private
|
255
|
+
|
256
|
+
def prev_scanline_byte x
|
257
|
+
if image.interlaced?
|
258
|
+
# When the image is interlaced, each pass of the interlace pattern is
|
259
|
+
# treated as an independent image for filtering purposes
|
260
|
+
image.adam7.pass_start?(@idx) ? 0 : image.scanlines[@idx-1].decoded_bytes[x].ord
|
261
|
+
elsif @idx > 0
|
262
|
+
image.scanlines[@idx-1].decoded_bytes[x].ord
|
263
|
+
else
|
264
|
+
0
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
214
268
|
def decode_byte x, b0, bpp1
|
215
269
|
raw = @image.imagedata[@offset+x+1]
|
216
270
|
|
@@ -220,27 +274,25 @@ module ZPNG
|
|
220
274
|
end
|
221
275
|
|
222
276
|
case @filter
|
223
|
-
when FILTER_NONE
|
277
|
+
when FILTER_NONE # 0
|
224
278
|
raw
|
225
279
|
|
226
|
-
when FILTER_SUB
|
280
|
+
when FILTER_SUB # 1
|
227
281
|
return raw unless b0
|
228
282
|
((raw.ord + b0.ord) & 0xff).chr
|
229
283
|
|
230
|
-
when FILTER_UP
|
231
|
-
|
232
|
-
prev = @image.scanlines[@idx-1].decoded_bytes[x]
|
233
|
-
((raw.ord + prev.ord) & 0xff).chr
|
284
|
+
when FILTER_UP # 2
|
285
|
+
((raw.ord + prev_scanline_byte(x)) & 0xff).chr
|
234
286
|
|
235
287
|
when FILTER_AVERAGE # 3
|
236
288
|
prev = (b0 && b0.ord) || 0
|
237
|
-
prior = (
|
289
|
+
prior = prev_scanline_byte(x)
|
238
290
|
((raw.ord + (prev + prior)/2) & 0xff).chr
|
239
291
|
|
240
|
-
when FILTER_PAETH
|
292
|
+
when FILTER_PAETH # 4
|
241
293
|
pa = (b0 && b0.ord) || 0
|
242
|
-
pb = (
|
243
|
-
pc =
|
294
|
+
pb = prev_scanline_byte(x)
|
295
|
+
pc = b0 ? prev_scanline_byte(x-bpp1) : 0
|
244
296
|
((raw.ord + paeth_predictor(pa, pb, pc)) & 0xff).chr
|
245
297
|
else
|
246
298
|
raise "invalid ScanLine filter #{@filter}"
|
data/lib/zpng/string_ext.rb
CHANGED
@@ -4,7 +4,15 @@ class String
|
|
4
4
|
[:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white].each do |color|
|
5
5
|
unless instance_methods.include?(color)
|
6
6
|
define_method color do
|
7
|
-
|
7
|
+
color(color)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
[:gray, :grey].each do |color|
|
13
|
+
unless instance_methods.include?(color)
|
14
|
+
define_method color do
|
15
|
+
color(:black).bright
|
8
16
|
end
|
9
17
|
end
|
10
18
|
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module ZPNG
|
2
|
+
class TextChunk < Chunk
|
3
|
+
attr_accessor :keyword, :text
|
4
|
+
|
5
|
+
def inspect verbosity = 10
|
6
|
+
vars = %w'keyword text language translated_keyword cmethod cflag'
|
7
|
+
vars -= %w'text translated_keyword' if verbosity <=0
|
8
|
+
super.sub(/ *>$/,'') + ", " +
|
9
|
+
vars.map do |var|
|
10
|
+
t = instance_variable_get("@#{var}")
|
11
|
+
unless t.is_a?(Fixnum)
|
12
|
+
t = t.to_s
|
13
|
+
t = t[0..20] + "..." if t.size > 20
|
14
|
+
end
|
15
|
+
if t.nil? || t == ''
|
16
|
+
nil
|
17
|
+
else
|
18
|
+
"#{var.to_s.tr('@','')}=#{t.inspect}"
|
19
|
+
end
|
20
|
+
end.compact.join(", ") + ">"
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_hash
|
24
|
+
{ :keyword => keyword, :text => text}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Chunk
|
29
|
+
class TEXT < TextChunk
|
30
|
+
def initialize *args
|
31
|
+
super
|
32
|
+
@keyword,@text = data.unpack('Z*a*')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class ZTXT < TextChunk
|
37
|
+
attr_accessor :cmethod # compression method
|
38
|
+
def initialize *args
|
39
|
+
super
|
40
|
+
@keyword,@cmethod,@text = data.unpack('Z*Ca*')
|
41
|
+
# current only @cmethod value is 0 - deflate
|
42
|
+
if @text
|
43
|
+
@text = Zlib::Inflate.inflate(@text)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class ITXT < TextChunk
|
49
|
+
attr_accessor :cflag, :cmethod # compression flag & method
|
50
|
+
attr_accessor :language, :translated_keyword
|
51
|
+
def initialize *args
|
52
|
+
super
|
53
|
+
# The text, unlike the other strings, is not null-terminated; its length is implied by the chunk length.
|
54
|
+
# http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.iTXt
|
55
|
+
@keyword, @cflag, @cmethod, @language, @translated_keyword, @text = data.unpack('Z*CCZ*Z*a*')
|
56
|
+
if @cflag == 1 && @cmethod == 0
|
57
|
+
@text = Zlib::Inflate.inflate(@text)
|
58
|
+
end
|
59
|
+
if @text
|
60
|
+
@text.force_encoding('utf-8') rescue nil
|
61
|
+
end
|
62
|
+
if @translated_keyword
|
63
|
+
@translated_keyword.force_encoding('utf-8') rescue nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_hash
|
68
|
+
super.tap do |h|
|
69
|
+
h[:language] = @language if @language || !@language.empty?
|
70
|
+
h[:translated_keyword] = @translated_keyword if @translated_keyword || !@translated_keyword.empty?
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/samples/itxt.png
ADDED
Binary file
|
data/spec/adam7_spec.rb
CHANGED
@@ -59,3 +59,27 @@ describe ZPNG::Adam7Decoder do
|
|
59
59
|
end
|
60
60
|
end
|
61
61
|
end
|
62
|
+
|
63
|
+
PNGSuite.each("???i*.png") do |fname_i|
|
64
|
+
fname_n = File.basename(fname_i)
|
65
|
+
fname_n[3] = 'n'
|
66
|
+
fname_n = File.join(File.dirname(fname_i), fname_n)
|
67
|
+
next unless File.exist?(fname_n)
|
68
|
+
|
69
|
+
describe fname_i.sub(%r|\A#{Regexp::escape(Dir.getwd)}/?|, '') do
|
70
|
+
it "should be pixel-by-pixel identical to " + fname_n.sub(%r|\A#{Regexp::escape(Dir.getwd)}/?|, '') do
|
71
|
+
interlaced = ZPNG::Image.load(fname_i)
|
72
|
+
normal = ZPNG::Image.load(fname_n)
|
73
|
+
|
74
|
+
normal.pixels.to_a.should == interlaced.pixels.to_a
|
75
|
+
|
76
|
+
interlaced.each_pixel do |color,x,y|
|
77
|
+
normal[x,y].should == color
|
78
|
+
end
|
79
|
+
|
80
|
+
normal.each_pixel do |color,x,y|
|
81
|
+
interlaced[x,y].should == color
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|