zpng 0.0.2 → 0.1.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.
- 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
|