zpng 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +3 -4
- data/Gemfile.lock +18 -16
- data/README.md +62 -48
- data/README.md.tpl +11 -1
- data/Rakefile +43 -0
- data/VERSION +1 -1
- data/lib/zpng/chunk.rb +114 -17
- data/lib/zpng/cli.rb +30 -5
- data/lib/zpng/color.rb +30 -0
- data/lib/zpng/image.rb +122 -19
- data/lib/zpng/scan_line.rb +138 -74
- data/samples/captcha_4bpp.png +0 -0
- data/spec/create_image_spec.rb +78 -0
- data/spec/crop_spec.rb +93 -0
- data/spec/image_spec.rb +54 -0
- data/spec/modify_spec.rb +2 -9
- data/spec/running_pixel_spec.rb +47 -0
- data/spec/spec_helper.rb +7 -1
- data/zpng.gemspec +17 -15
- metadata +57 -38
data/lib/zpng/cli.rb
CHANGED
@@ -6,8 +6,8 @@ require 'pp'
|
|
6
6
|
class ZPNG::CLI
|
7
7
|
|
8
8
|
ACTIONS = {
|
9
|
-
'info' => 'General image info',
|
10
9
|
'chunks' => 'Show file chunks (default)',
|
10
|
+
%w'i info' => 'General image info (default)',
|
11
11
|
'ascii' => 'Try to display image as ASCII (works best with monochrome images)',
|
12
12
|
'scanlines' => 'Show scanlines info',
|
13
13
|
'palette' => 'Show palette'
|
@@ -22,7 +22,7 @@ class ZPNG::CLI
|
|
22
22
|
@actions = []
|
23
23
|
@options = { :verbose => 0 }
|
24
24
|
optparser = OptionParser.new do |opts|
|
25
|
-
opts.banner = "Usage: zpng [options]"
|
25
|
+
opts.banner = "Usage: zpng [options] filename.png"
|
26
26
|
|
27
27
|
opts.on "-v", "--verbose", "Run verbosely (can be used multiple times)" do |v|
|
28
28
|
@options[:verbose] += 1
|
@@ -32,12 +32,24 @@ class ZPNG::CLI
|
|
32
32
|
end
|
33
33
|
|
34
34
|
ACTIONS.each do |t,desc|
|
35
|
-
|
35
|
+
if t.is_a?(Array)
|
36
|
+
opts.on *[ "-#{t[0]}", "--#{t[1]}", desc, eval("lambda{ |_| @actions << :#{t[1]} }") ]
|
37
|
+
else
|
38
|
+
opts.on *[ "-#{t[0].upcase}", "--#{t}", desc, eval("lambda{ |_| @actions << :#{t} }") ]
|
39
|
+
end
|
36
40
|
end
|
37
41
|
|
38
|
-
opts.on "-E", "--extract-chunk
|
42
|
+
opts.on "-E", "--extract-chunk ID", "extract a single chunk" do |id|
|
39
43
|
@actions << [:extract_chunk, id.to_i]
|
40
44
|
end
|
45
|
+
opts.on "-U", "--unpack-imagedata", "unpack Image Data (IDAT) chunk(s), output to stdout" do
|
46
|
+
@actions << :unpack_imagedata
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.on "-c", "--crop GEOMETRY", "crop image, {WIDTH}x{HEIGHT}+{X}+{Y},",
|
50
|
+
"puts results on stdout unless --ascii given" do |x|
|
51
|
+
@actions << [:crop, x]
|
52
|
+
end
|
41
53
|
end
|
42
54
|
|
43
55
|
if (argv = optparser.parse(@argv)).empty?
|
@@ -80,12 +92,25 @@ class ZPNG::CLI
|
|
80
92
|
end
|
81
93
|
end
|
82
94
|
|
95
|
+
def unpack_imagedata
|
96
|
+
print @img.imagedata
|
97
|
+
end
|
98
|
+
|
99
|
+
def crop geometry
|
100
|
+
unless geometry =~ /\A(\d+)x(\d+)\+(\d+)\+(\d+)\Z/i
|
101
|
+
STDERR.puts "[!] invalid geometry #{geometry.inspect}, must be WxH+X+Y, like 100x100+10+10"
|
102
|
+
exit 1
|
103
|
+
end
|
104
|
+
@img.crop! :width => $1.to_i, :height => $2.to_i, :x => $3.to_i, :y => $4.to_i
|
105
|
+
print @img.export unless @actions.include?(:ascii)
|
106
|
+
end
|
107
|
+
|
83
108
|
def load_file fname
|
84
109
|
@img = ZPNG::Image.new fname
|
85
110
|
end
|
86
111
|
|
87
112
|
def info
|
88
|
-
puts "[.] image size #{@img.width}x#{@img.height}"
|
113
|
+
puts "[.] image size #{@img.width || '?'}x#{@img.height || '?'}"
|
89
114
|
puts "[.] uncompressed imagedata size = #{@img.imagedata.size} bytes"
|
90
115
|
puts "[.] palette = #{@img.palette}" if @img.palette
|
91
116
|
end
|
data/lib/zpng/color.rb
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
module ZPNG
|
2
2
|
class Color < Struct.new(:r,:g,:b,:a)
|
3
3
|
|
4
|
+
def initialize *args
|
5
|
+
super
|
6
|
+
self.a ||= 0xff
|
7
|
+
end
|
8
|
+
|
4
9
|
alias :alpha :a
|
10
|
+
def alpha= v; self.a = v; end
|
5
11
|
|
6
12
|
BLACK = Color.new(0,0,0,0xff)
|
7
13
|
WHITE = Color.new(0xff,0xff,0xff,0xff)
|
@@ -14,12 +20,36 @@ module ZPNG
|
|
14
20
|
r == 0 && g == 0 && b == 0
|
15
21
|
end
|
16
22
|
|
23
|
+
def transparent?
|
24
|
+
a == 0
|
25
|
+
end
|
26
|
+
|
17
27
|
def to_grayscale
|
18
28
|
(r+g+b)/3
|
19
29
|
end
|
20
30
|
|
31
|
+
def self.from_grayscale value, alpha = nil
|
32
|
+
Color.new value,value,value, alpha
|
33
|
+
end
|
34
|
+
|
21
35
|
def to_s
|
22
36
|
"%02X%02X%02X" % [r,g,b]
|
23
37
|
end
|
38
|
+
|
39
|
+
def to_i
|
40
|
+
((a||0) << 24) + ((r||0) << 16) + ((g||0) << 8) + (b||0)
|
41
|
+
end
|
42
|
+
|
43
|
+
def inspect
|
44
|
+
if r && g && b && a
|
45
|
+
"#<ZPNG::Color #%02x%02x%02x a=%d>" % [r,g,b,a]
|
46
|
+
else
|
47
|
+
rs = r ? "%02x" % r : "??"
|
48
|
+
gs = g ? "%02x" % g : "??"
|
49
|
+
bs = b ? "%02x" % b : "??"
|
50
|
+
as = a ? "%d" % a : "?"
|
51
|
+
"#<ZPNG::Color #%s%s%s%s a=%s>" % [rs,gs,bs,as]
|
52
|
+
end
|
53
|
+
end
|
24
54
|
end
|
25
55
|
end
|
data/lib/zpng/image.rb
CHANGED
@@ -5,7 +5,55 @@ module ZPNG
|
|
5
5
|
|
6
6
|
PNG_HDR = "\x89PNG\x0d\x0a\x1a\x0a"
|
7
7
|
|
8
|
-
def initialize x
|
8
|
+
def initialize x
|
9
|
+
@chunks = []
|
10
|
+
case x
|
11
|
+
when IO
|
12
|
+
_from_string x.read
|
13
|
+
when String
|
14
|
+
_from_string x
|
15
|
+
when Hash
|
16
|
+
_from_hash x
|
17
|
+
else
|
18
|
+
raise "unsupported input data type #{x.class}"
|
19
|
+
end
|
20
|
+
if palette && hdr && hdr.depth
|
21
|
+
palette.max_colors = 2**hdr.depth
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# load image from file
|
26
|
+
def self.load fname
|
27
|
+
open(fname,"rb") do |f|
|
28
|
+
Image.new(f)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
alias :load_file :load
|
32
|
+
|
33
|
+
# save image to file
|
34
|
+
def save fname
|
35
|
+
File.open(fname,"wb"){ |f| f << export }
|
36
|
+
end
|
37
|
+
|
38
|
+
# flag that image is just created, and NOT loaded from file
|
39
|
+
# as in Rails' ActiveRecord::Base#new_record?
|
40
|
+
def new_image?
|
41
|
+
@new_image
|
42
|
+
end
|
43
|
+
alias :new? :new_image?
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def _from_hash h
|
48
|
+
@new_image = true
|
49
|
+
@chunks << (@header = Chunk::IHDR.new(h))
|
50
|
+
if @header.palette_used?
|
51
|
+
@chunks << (@palette = Chunk::PLTE.new)
|
52
|
+
@palette[0] = h[:bg] || Color::BLACK # add default bg color
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def _from_string x
|
9
57
|
if x
|
10
58
|
if x[PNG_HDR]
|
11
59
|
# raw image data
|
@@ -23,7 +71,6 @@ module ZPNG
|
|
23
71
|
|
24
72
|
io = StringIO.new(data)
|
25
73
|
io.seek PNG_HDR.size
|
26
|
-
@chunks = []
|
27
74
|
while !io.eof?
|
28
75
|
chunk = Chunk.from_stream(io)
|
29
76
|
chunk.idx = @chunks.size
|
@@ -44,6 +91,7 @@ module ZPNG
|
|
44
91
|
end
|
45
92
|
end
|
46
93
|
|
94
|
+
public
|
47
95
|
def dump
|
48
96
|
@chunks.each do |chunk|
|
49
97
|
puts "[.] #{chunk.inspect} #{chunk.crc_ok? ? 'CRC OK'.green : 'CRC ERROR'.red}"
|
@@ -58,13 +106,21 @@ module ZPNG
|
|
58
106
|
@header && @header.height
|
59
107
|
end
|
60
108
|
|
109
|
+
def grayscale?
|
110
|
+
@header && @header.grayscale?
|
111
|
+
end
|
112
|
+
|
61
113
|
def imagedata
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
114
|
+
@imagedata ||=
|
115
|
+
begin
|
116
|
+
if @header
|
117
|
+
raise "only non-interlaced mode is supported for imagedata" if @header.interlace != 0
|
118
|
+
else
|
119
|
+
puts "[?] no image header, assuming non-interlaced RGB".yellow
|
120
|
+
end
|
121
|
+
data = @chunks.find_all{ |c| c.is_a?(Chunk::IDAT) }.map(&:data).join
|
122
|
+
(data && data.size > 0) ? Zlib::Inflate.inflate(data) : ''
|
123
|
+
end
|
68
124
|
end
|
69
125
|
|
70
126
|
def [] x, y
|
@@ -72,30 +128,36 @@ module ZPNG
|
|
72
128
|
end
|
73
129
|
|
74
130
|
def []= x, y, newpixel
|
75
|
-
|
76
|
-
# or scanlines decoded AFTER modification of UPPER ones will be decoded wrong
|
77
|
-
_decode_all_scanlines unless @_all_scanlines_decoded
|
131
|
+
decode_all_scanlines
|
78
132
|
scanlines[y][x] = newpixel
|
79
133
|
end
|
80
134
|
|
81
|
-
|
135
|
+
# we must decode all scanlines before doing any modifications
|
136
|
+
# or scanlines decoded AFTER modification of UPPER ones will be decoded wrong
|
137
|
+
def decode_all_scanlines
|
138
|
+
return if @all_scanlines_decoded
|
139
|
+
@all_scanlines_decoded = true
|
82
140
|
scanlines.each(&:decode!)
|
83
|
-
@_all_scanlines_decoded = true
|
84
141
|
end
|
85
142
|
|
86
143
|
def scanlines
|
87
144
|
@scanlines ||=
|
88
145
|
begin
|
89
146
|
r = []
|
90
|
-
height.times do |i|
|
147
|
+
height.to_i.times do |i|
|
91
148
|
r << ScanLine.new(self,i)
|
92
149
|
end
|
150
|
+
r.delete_if(&:bad?)
|
93
151
|
r
|
94
152
|
end
|
95
153
|
end
|
96
154
|
|
97
155
|
def to_s h={}
|
98
|
-
scanlines.
|
156
|
+
if scanlines.any?
|
157
|
+
scanlines.map{ |l| l.to_s(h) }.join("\n")
|
158
|
+
else
|
159
|
+
super()
|
160
|
+
end
|
99
161
|
end
|
100
162
|
|
101
163
|
def extract_block x,y=nil,w=nil,h=nil
|
@@ -118,14 +180,55 @@ module ZPNG
|
|
118
180
|
def export
|
119
181
|
imagedata # fill @imagedata, if not already filled
|
120
182
|
|
121
|
-
# delete
|
122
|
-
|
123
|
-
@chunks.delete_if{ |c| c.type == 'IDAT' && c != first_idat }
|
183
|
+
# delete old IDAT chunks
|
184
|
+
@chunks.delete_if{ |c| c.is_a?(Chunk::IDAT) }
|
124
185
|
|
125
186
|
# fill first_idat @data with compressed imagedata
|
126
|
-
|
187
|
+
@chunks << Chunk::IDAT.new(
|
188
|
+
:data => Zlib::Deflate.deflate(scanlines.map(&:export).join, 9)
|
189
|
+
)
|
190
|
+
|
191
|
+
# delete IEND chunk(s) b/c we just added a new chunk and IEND must be the last one
|
192
|
+
@chunks.delete_if{ |c| c.is_a?(Chunk::IEND) }
|
193
|
+
|
194
|
+
# add fresh new IEND
|
195
|
+
@chunks << Chunk::IEND.new
|
127
196
|
|
128
197
|
PNG_HDR + @chunks.map(&:export).join
|
129
198
|
end
|
199
|
+
|
200
|
+
# modifies this image
|
201
|
+
def crop! params
|
202
|
+
decode_all_scanlines
|
203
|
+
|
204
|
+
x,y,h,w = (params[:x]||0), (params[:y]||0), params[:height], params[:width]
|
205
|
+
raise "negative params not allowed" if [x,y,h,w].any?{ |x| x < 0 }
|
206
|
+
|
207
|
+
# adjust crop sizes if they greater than image sizes
|
208
|
+
h = self.height-y if (y+h) > self.height
|
209
|
+
w = self.width-x if (x+w) > self.width
|
210
|
+
raise "negative params not allowed (p2)" if [x,y,h,w].any?{ |x| x < 0 }
|
211
|
+
|
212
|
+
# delete excess scanlines at tail
|
213
|
+
scanlines[(y+h)..-1] = [] if (y+h) < scanlines.size
|
214
|
+
|
215
|
+
# delete excess scanlines at head
|
216
|
+
scanlines[0,y] = [] if y > 0
|
217
|
+
|
218
|
+
# crop remaining scanlines
|
219
|
+
scanlines.each{ |l| l.crop!(x,w) }
|
220
|
+
|
221
|
+
# modify header
|
222
|
+
hdr.height, hdr.width = h, w
|
223
|
+
|
224
|
+
# return self
|
225
|
+
self
|
226
|
+
end
|
227
|
+
|
228
|
+
# returns new image
|
229
|
+
def crop params
|
230
|
+
# deep copy first, then crop!
|
231
|
+
Marshal.load(Marshal.dump(self)).crop!(params)
|
232
|
+
end
|
130
233
|
end
|
131
234
|
end
|
data/lib/zpng/scan_line.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
#coding: binary
|
1
2
|
module ZPNG
|
2
3
|
class ScanLine
|
3
4
|
FILTER_NONE = 0
|
@@ -12,17 +13,41 @@ module ZPNG
|
|
12
13
|
@image,@idx = image,idx
|
13
14
|
@bpp = image.hdr.bpp
|
14
15
|
raise "[!] zero bpp" if @bpp == 0
|
15
|
-
|
16
|
-
|
16
|
+
|
17
|
+
# Bytes Per Pixel, if bpp = 8, 16, 24, 32
|
18
|
+
# NULL otherwise
|
19
|
+
@BPP = (@bpp%8 == 0) && (@bpp>>3)
|
20
|
+
|
21
|
+
if @image.new?
|
22
|
+
@decoded_bytes = "\x00" * (size-1)
|
23
|
+
@filter = FILTER_NONE
|
17
24
|
else
|
18
|
-
@offset = idx*
|
25
|
+
@offset = idx*size
|
26
|
+
if @filter = image.imagedata[@offset]
|
27
|
+
@filter = @filter.ord
|
28
|
+
else
|
29
|
+
STDERR.puts "[!] #{self.class}: ##@idx: no data at pos 0, scanline dropped".red
|
30
|
+
end
|
31
|
+
@offset += 1
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# ScanLine is BAD if it has no filter
|
36
|
+
def bad?
|
37
|
+
!@filter
|
38
|
+
end
|
39
|
+
|
40
|
+
# total scanline size in bytes, INCLUDING leading 'filter' byte
|
41
|
+
def size
|
42
|
+
if @BPP
|
43
|
+
image.width*@BPP+1
|
44
|
+
else
|
45
|
+
(image.width*@bpp/8.0+1).ceil
|
19
46
|
end
|
20
|
-
@filter = image.imagedata[@offset].ord
|
21
|
-
@offset += 1
|
22
47
|
end
|
23
48
|
|
24
49
|
def inspect
|
25
|
-
"#<ZPNG::ScanLine " + (instance_variables-[:@image, :@decoded]).
|
50
|
+
"#<ZPNG::ScanLine " + (instance_variables-[:@image, :@decoded, :@BPP]).
|
26
51
|
map{ |var| "#{var.to_s.tr('@','')}=#{instance_variable_get(var)}" }.
|
27
52
|
join(", ") + ">"
|
28
53
|
end
|
@@ -34,6 +59,7 @@ module ZPNG
|
|
34
59
|
|
35
60
|
@image.width.times.map do |i|
|
36
61
|
px = decode_pixel(i)
|
62
|
+
# printf "[d] %08x %s %s\n", px.to_i, px.inspect, px.to_s unless px.black?
|
37
63
|
px.white?? white : (px.black?? black : unknown)
|
38
64
|
end.join
|
39
65
|
end
|
@@ -43,40 +69,37 @@ module ZPNG
|
|
43
69
|
end
|
44
70
|
|
45
71
|
def []= x, newcolor
|
72
|
+
if image.hdr.palette_used?
|
73
|
+
color_idx = image.palette.find_or_add(newcolor)
|
74
|
+
raise "no color #{newcolor.inspect} in palette" unless color_idx
|
75
|
+
elsif image.grayscale?
|
76
|
+
color_idx = newcolor.to_grayscale
|
77
|
+
end
|
78
|
+
|
46
79
|
case @bpp
|
47
|
-
when 1
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
raise "1bpp pixel can only be WHITE or BLACK, got #{newcolor.inspect}"
|
60
|
-
end
|
61
|
-
end
|
62
|
-
if flag
|
63
|
-
# turn pixel on
|
64
|
-
decoded_bytes[x/8] = (decoded_bytes[x/8].ord | (1<<(7-(x%8)))).chr
|
65
|
-
else
|
66
|
-
# turn pixel off
|
67
|
-
decoded_bytes[x/8] = (decoded_bytes[x/8].ord & (0xff-(1<<(7-(x%8))))).chr
|
68
|
-
end
|
80
|
+
when 1,2,4
|
81
|
+
pos = x*@bpp/8
|
82
|
+
b = decoded_bytes[pos].ord
|
83
|
+
mask = 2**@bpp-1
|
84
|
+
shift = 8-(x%(8/@bpp)+1)*@bpp
|
85
|
+
raise "invalid shift #{shift}" if shift < 0 || shift > 7
|
86
|
+
|
87
|
+
# printf "[d] %s x=%2d bpp=%d pos=%d mask=%08b shift=%d decoded_bytes=#{decoded_bytes.inspect}\n", self.to_s, x, @bpp, pos, mask, shift
|
88
|
+
|
89
|
+
b = (b & (0xff-(mask<<shift))) | ((color_idx & mask) << shift)
|
90
|
+
decoded_bytes[pos] = b.chr
|
91
|
+
|
69
92
|
when 8
|
70
93
|
if image.hdr.palette_used?
|
71
|
-
decoded_bytes[x] =
|
94
|
+
decoded_bytes[x] = color_idx.chr
|
72
95
|
else
|
73
96
|
decoded_bytes[x] = ((newcolor.r + newcolor.g + newcolor.b)/3).chr
|
74
97
|
end
|
75
98
|
when 16
|
76
99
|
if image.hdr.palette_used? && image.hdr.alpha_used?
|
77
|
-
decoded_bytes[x*2] =
|
100
|
+
decoded_bytes[x*2] = color_idx.chr
|
78
101
|
decoded_bytes[x*2+1] = (newcolor.alpha || 0xff).chr
|
79
|
-
elsif image.
|
102
|
+
elsif image.grayscale? && image.hdr.alpha_used?
|
80
103
|
decoded_bytes[x*2] = newcolor.to_grayscale.chr
|
81
104
|
decoded_bytes[x*2+1] = (newcolor.alpha || 0xff).chr
|
82
105
|
else
|
@@ -97,56 +120,51 @@ module ZPNG
|
|
97
120
|
decoded_bytes[x*@BPP, @BPP]
|
98
121
|
else
|
99
122
|
# 1, 2 or 4 bits per pixel
|
100
|
-
decoded_bytes[x*@bpp/8
|
123
|
+
decoded_bytes[x*@bpp/8]
|
101
124
|
end
|
102
125
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
return image.palette[idx]
|
121
|
-
end
|
122
|
-
|
123
|
-
case @bpp
|
124
|
-
when 1
|
125
|
-
r=g=b= (raw.ord & (1<<(7-(x%8)))) == 0 ? 0 : 0xff
|
126
|
-
when 8
|
127
|
-
if colormode == ZPNG::Chunk::IHDR::COLOR_GRAYSCALE
|
128
|
-
r=g=b= raw.ord
|
126
|
+
color, alpha =
|
127
|
+
case @bpp
|
128
|
+
when 1,2,4
|
129
|
+
mask = 2**@bpp-1
|
130
|
+
shift = 8-(x%(8/@bpp)+1)*@bpp
|
131
|
+
raise "invalid shift #{shift}" if shift < 0 || shift > 7
|
132
|
+
[(raw.ord >> shift) & mask, nil]
|
133
|
+
when 8
|
134
|
+
[raw.ord, nil]
|
135
|
+
when 16
|
136
|
+
raw.unpack 'C2'
|
137
|
+
when 24
|
138
|
+
# RGB
|
139
|
+
return Color.new(*raw.unpack('C3'))
|
140
|
+
when 32
|
141
|
+
# RGBA
|
142
|
+
return Color.new(*raw.unpack('C4'))
|
129
143
|
else
|
130
|
-
raise "unexpected
|
144
|
+
raise "unexpected bpp #{@bpp}"
|
131
145
|
end
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
a
|
136
|
-
|
137
|
-
|
146
|
+
|
147
|
+
if image.grayscale?
|
148
|
+
if [1,2,4].include?(@bpp)
|
149
|
+
#color should be extended to a 8-bit range
|
150
|
+
if color%2 == 0
|
151
|
+
color <<= (8-@bpp)
|
152
|
+
else
|
153
|
+
(8-@bpp).times{ color = color*2 + 1 }
|
154
|
+
end
|
138
155
|
end
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
156
|
+
Color.from_grayscale(color, alpha)
|
157
|
+
elsif image.palette
|
158
|
+
color = image.palette[color]
|
159
|
+
color.alpha = alpha
|
160
|
+
color
|
161
|
+
else
|
162
|
+
raise "cannot decode color"
|
144
163
|
end
|
145
|
-
|
146
|
-
Color.new(r,g,b,a)
|
147
164
|
end
|
148
165
|
|
149
166
|
def decoded_bytes
|
167
|
+
raise if caller.size > 50
|
150
168
|
@decoded_bytes ||=
|
151
169
|
begin
|
152
170
|
# number of bytes per complete pixel, rounding up to one
|
@@ -170,11 +188,12 @@ module ZPNG
|
|
170
188
|
true
|
171
189
|
end
|
172
190
|
|
191
|
+
private
|
173
192
|
def decode_byte x, b0, bpp1
|
174
193
|
raw = @image.imagedata[@offset+x]
|
175
194
|
|
176
195
|
unless raw
|
177
|
-
STDERR.puts "[!]
|
196
|
+
STDERR.puts "[!] #{self.class}: ##@idx: no data at pos #{x}".red
|
178
197
|
raw = 0.chr
|
179
198
|
end
|
180
199
|
|
@@ -214,9 +233,54 @@ module ZPNG
|
|
214
233
|
(pa <= pb) ? (pa <= pc ? a : c) : (pb <= pc ? b : c)
|
215
234
|
end
|
216
235
|
|
236
|
+
public
|
237
|
+
def crop! x, w
|
238
|
+
if @BPP
|
239
|
+
# great, crop is byte-aligned! :)
|
240
|
+
decoded_bytes[0,x*@BPP] = ''
|
241
|
+
decoded_bytes[w*@BPP..-1] = ''
|
242
|
+
else
|
243
|
+
# oh, no we have to shift bits in a whole line :(
|
244
|
+
case @bpp
|
245
|
+
when 1,2,4
|
246
|
+
cut_bits_head = @bpp*x
|
247
|
+
if cut_bits_head > 8
|
248
|
+
# cut whole head bytes
|
249
|
+
decoded_bytes[0,cut_bits_head/8] = ''
|
250
|
+
end
|
251
|
+
cut_bits_head %= 8
|
252
|
+
if cut_bits_head > 0
|
253
|
+
# bit-shift all remaining bytes
|
254
|
+
(w*@bpp/8.0).ceil.times do |i|
|
255
|
+
decoded_bytes[i] = ((
|
256
|
+
(decoded_bytes[i].ord<<cut_bits_head) |
|
257
|
+
(decoded_bytes[i+1].ord>>(8-cut_bits_head))
|
258
|
+
) & 0xff).chr
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
new_width_bits = w*@bpp
|
263
|
+
diff = decoded_bytes.size*8 - new_width_bits
|
264
|
+
raise if diff < 0
|
265
|
+
if diff > 8
|
266
|
+
# cut whole tail bytes
|
267
|
+
decoded_bytes[(new_width_bits/8.0).ceil..-1] = ''
|
268
|
+
end
|
269
|
+
diff %= 8
|
270
|
+
if diff > 0
|
271
|
+
# zero tail bits of last byte
|
272
|
+
decoded_bytes[-1] = (decoded_bytes[-1].ord & (0xff-(2**diff)+1)).chr
|
273
|
+
end
|
274
|
+
|
275
|
+
else
|
276
|
+
raise "unexpected bpp=#@bpp"
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
217
281
|
def export
|
218
282
|
# we export in FILTER_NONE mode
|
219
|
-
|
283
|
+
FILTER_NONE.chr + decoded_bytes
|
220
284
|
end
|
221
285
|
end
|
222
286
|
end
|