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
@@ -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
|