zpng 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +32 -0
- data/LICENSE.txt +20 -0
- data/README.md +161 -0
- data/README.md.tpl +65 -0
- data/Rakefile +74 -0
- data/VERSION +1 -0
- data/bin/zpng +7 -0
- data/lib/zpng.rb +41 -0
- data/lib/zpng/block.rb +41 -0
- data/lib/zpng/chunk.rb +133 -0
- data/lib/zpng/cli.rb +90 -0
- data/lib/zpng/color.rb +25 -0
- data/lib/zpng/image.rb +130 -0
- data/lib/zpng/scan_line.rb +222 -0
- data/samples/modify.rb +20 -0
- data/samples/qr_aux_chunks.png +0 -0
- data/samples/qr_bw.png +0 -0
- data/samples/qr_gray_alpha.png +0 -0
- data/samples/qr_grayscale.png +0 -0
- data/samples/qr_plte.png +0 -0
- data/samples/qr_plte_bw.png +0 -0
- data/samples/qr_rgb.png +0 -0
- data/samples/qr_rgba.png +0 -0
- data/spec/ascii_spec.rb +52 -0
- data/spec/modify_spec.rb +63 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/zpng_spec.rb +7 -0
- metadata +148 -0
data/lib/zpng/chunk.rb
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
module ZPNG
|
2
|
+
class Chunk
|
3
|
+
attr_accessor :size, :type, :data, :crc
|
4
|
+
|
5
|
+
def self.from_stream io
|
6
|
+
size, type = io.read(8).unpack('Na4')
|
7
|
+
io.seek(-8,IO::SEEK_CUR)
|
8
|
+
begin
|
9
|
+
if const_defined?(type.upcase)
|
10
|
+
klass = const_get(type.upcase)
|
11
|
+
klass.new(io)
|
12
|
+
else
|
13
|
+
Chunk.new(io)
|
14
|
+
end
|
15
|
+
rescue NameError
|
16
|
+
# invalid chunk type?
|
17
|
+
Chunk.new(io)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize io
|
22
|
+
@size, @type = io.read(8).unpack('Na4')
|
23
|
+
@data = io.read(size)
|
24
|
+
@crc = io.read(4).to_s.unpack('N').first
|
25
|
+
end
|
26
|
+
|
27
|
+
def export
|
28
|
+
@data = self.export_data # virtual
|
29
|
+
@crc = Zlib.crc32(data, Zlib.crc32(type))
|
30
|
+
[@size,@type].pack('Na4') + @data + [@crc].pack('N')
|
31
|
+
end
|
32
|
+
|
33
|
+
def export_data
|
34
|
+
#STDERR.puts "[!] Chunk::#{type} must realize 'export_data' virtual method".yellow if @size != 0
|
35
|
+
@data
|
36
|
+
end
|
37
|
+
|
38
|
+
def inspect
|
39
|
+
size = @size ? sprintf("%5d",@size) : sprintf("%5s","???")
|
40
|
+
crc = @crc ? sprintf("%08x",@crc) : sprintf("%8s","???")
|
41
|
+
type = @type.to_s.gsub(/[^0-9a-z]/i){ |x| sprintf("\\x%02X",x.ord) }
|
42
|
+
sprintf("#<ZPNG::Chunk %4s size=%s, crc=%s >", type, size, crc)
|
43
|
+
end
|
44
|
+
|
45
|
+
def crc_ok?
|
46
|
+
expected_crc = Zlib.crc32(data, Zlib.crc32(type))
|
47
|
+
expected_crc == crc
|
48
|
+
end
|
49
|
+
|
50
|
+
class IHDR < Chunk
|
51
|
+
attr_accessor :width, :height, :depth, :color, :compression, :filter, :interlace
|
52
|
+
|
53
|
+
PALETTE_USED = 1
|
54
|
+
COLOR_USED = 2
|
55
|
+
ALPHA_USED = 4
|
56
|
+
|
57
|
+
COLOR_GRAYSCALE = 0 # Each pixel is a grayscale sample
|
58
|
+
COLOR_RGB = 2 # Each pixel is an R,G,B triple.
|
59
|
+
COLOR_INDEXED = 3 # Each pixel is a palette index; a PLTE chunk must appear.
|
60
|
+
COLOR_GRAY_ALPHA = 4 # Each pixel is a grayscale sample, followed by an alpha sample.
|
61
|
+
COLOR_RGBA = 6 # Each pixel is an R,G,B triple, followed by an alpha sample.
|
62
|
+
|
63
|
+
SAMPLES_PER_COLOR = {
|
64
|
+
COLOR_GRAYSCALE => 1,
|
65
|
+
COLOR_RGB => 3,
|
66
|
+
COLOR_INDEXED => 1,
|
67
|
+
COLOR_GRAY_ALPHA => 2,
|
68
|
+
COLOR_RGBA => 4
|
69
|
+
}
|
70
|
+
|
71
|
+
FORMAT = 'NNC5'
|
72
|
+
|
73
|
+
def initialize io
|
74
|
+
super
|
75
|
+
@width, @height, @depth, @color, @compression, @filter, @interlace = data.unpack(FORMAT)
|
76
|
+
end
|
77
|
+
|
78
|
+
def export_data
|
79
|
+
[@width, @height, @depth, @color, @compression, @filter, @interlace].pack(FORMAT)
|
80
|
+
end
|
81
|
+
|
82
|
+
# bits per pixel
|
83
|
+
def bpp
|
84
|
+
SAMPLES_PER_COLOR[@color] * depth
|
85
|
+
end
|
86
|
+
|
87
|
+
def color_used?
|
88
|
+
(@color & COLOR_USED) != 0
|
89
|
+
end
|
90
|
+
|
91
|
+
def grayscale?
|
92
|
+
!color_used?
|
93
|
+
end
|
94
|
+
|
95
|
+
def palette_used?
|
96
|
+
(@color & PALETTE_USED) != 0
|
97
|
+
end
|
98
|
+
|
99
|
+
def alpha_used?
|
100
|
+
(@color & ALPHA_USED) != 0
|
101
|
+
end
|
102
|
+
|
103
|
+
def inspect
|
104
|
+
super.sub(/ *>$/,'') + ", " +
|
105
|
+
(instance_variables-[:@type, :@crc, :@data, :@size]).
|
106
|
+
map{ |var| "#{var.to_s.tr('@','')}=#{instance_variable_get(var)}" }.
|
107
|
+
join(", ") + ">"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class PLTE < Chunk
|
112
|
+
def [] idx
|
113
|
+
rgb = @data[idx*3,3]
|
114
|
+
rgb && ZPNG::Color.new(*rgb.split('').map(&:ord))
|
115
|
+
end
|
116
|
+
|
117
|
+
def ncolors
|
118
|
+
@size/3
|
119
|
+
end
|
120
|
+
|
121
|
+
def index color
|
122
|
+
ncolors.times do |i|
|
123
|
+
c = self[i]
|
124
|
+
return i if c.r == color.r && c.g == color.g && c.b == color.b
|
125
|
+
end
|
126
|
+
nil
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class IEND < Chunk
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/lib/zpng/cli.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'zpng'
|
2
|
+
require 'optparse'
|
3
|
+
require 'hexdump'
|
4
|
+
require 'pp'
|
5
|
+
|
6
|
+
class ZPNG::CLI
|
7
|
+
|
8
|
+
ACTIONS = {
|
9
|
+
'info' => 'General image info',
|
10
|
+
'chunks' => 'Show file chunks (default)',
|
11
|
+
'ascii' => 'Try to display image as ASCII (works best with monochrome images)',
|
12
|
+
'scanlines' => 'Show scanlines info',
|
13
|
+
'palette' => 'Show palette'
|
14
|
+
}
|
15
|
+
DEFAULT_ACTIONS = %w'info chunks'
|
16
|
+
|
17
|
+
def initialize argv = ARGV
|
18
|
+
@argv = argv
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
@actions = []
|
23
|
+
@options = { :verbose => 0 }
|
24
|
+
optparser = OptionParser.new do |opts|
|
25
|
+
opts.banner = "Usage: zpng [options]"
|
26
|
+
|
27
|
+
opts.on "-v", "--verbose", "Run verbosely (can be used multiple times)" do |v|
|
28
|
+
@options[:verbose] += 1
|
29
|
+
end
|
30
|
+
opts.on "-q", "--quiet", "Silent any warnings (can be used multiple times)" do |v|
|
31
|
+
@options[:verbose] -= 1
|
32
|
+
end
|
33
|
+
|
34
|
+
ACTIONS.each do |t,desc|
|
35
|
+
opts.on *[ "-#{t[0].upcase}", "--#{t}", desc, eval("lambda{ |_| @actions << :#{t} }") ]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
if (argv = optparser.parse(@argv)).empty?
|
40
|
+
puts optparser.help
|
41
|
+
return
|
42
|
+
end
|
43
|
+
|
44
|
+
@actions = DEFAULT_ACTIONS if @actions.empty?
|
45
|
+
|
46
|
+
argv.each_with_index do |fname,idx|
|
47
|
+
@need_fname_header = (argv.size > 1)
|
48
|
+
@file_idx = idx
|
49
|
+
@file_name = fname
|
50
|
+
|
51
|
+
@zpng = load_file fname
|
52
|
+
|
53
|
+
@actions.each do |action|
|
54
|
+
self.send(action) if self.respond_to?(action)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
rescue Errno::EPIPE
|
58
|
+
# output interrupt, f.ex. when piping output to a 'head' command
|
59
|
+
# prevents a 'Broken pipe - <STDOUT> (Errno::EPIPE)' message
|
60
|
+
end
|
61
|
+
|
62
|
+
def load_file fname
|
63
|
+
@img = ZPNG::Image.new fname
|
64
|
+
end
|
65
|
+
|
66
|
+
def info
|
67
|
+
puts "[.] image size #{@img.width}x#{@img.height}"
|
68
|
+
puts "[.] uncompressed imagedata size = #{@img.imagedata.size} bytes"
|
69
|
+
puts "[.] palette = #{@img.palette}" if @img.palette
|
70
|
+
end
|
71
|
+
|
72
|
+
def chunks
|
73
|
+
@img.dump
|
74
|
+
end
|
75
|
+
|
76
|
+
def ascii
|
77
|
+
puts @img.to_s
|
78
|
+
end
|
79
|
+
|
80
|
+
def scanlines
|
81
|
+
pp @img.scanlines
|
82
|
+
end
|
83
|
+
|
84
|
+
def palette
|
85
|
+
if @img.palette
|
86
|
+
pp @img.palette
|
87
|
+
Hexdump.dump @img.palette.data, :width => 6*3
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/lib/zpng/color.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module ZPNG
|
2
|
+
class Color < Struct.new(:r,:g,:b,:a)
|
3
|
+
|
4
|
+
alias :alpha :a
|
5
|
+
|
6
|
+
BLACK = Color.new(0,0,0,0xff)
|
7
|
+
WHITE = Color.new(0xff,0xff,0xff,0xff)
|
8
|
+
|
9
|
+
def white?
|
10
|
+
r == 0xff && g == 0xff && b == 0xff
|
11
|
+
end
|
12
|
+
|
13
|
+
def black?
|
14
|
+
r == 0 && g == 0 && b == 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_grayscale
|
18
|
+
(r+g+b)/3
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
"%02X%02X%02X" % [r,g,b]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/zpng/image.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
module ZPNG
|
2
|
+
class Image
|
3
|
+
attr_accessor :data, :header, :chunks, :scanlines, :imagedata, :palette
|
4
|
+
alias :hdr :header
|
5
|
+
|
6
|
+
PNG_HDR = "\x89PNG\x0d\x0a\x1a\x0a"
|
7
|
+
|
8
|
+
def initialize x = nil
|
9
|
+
if x
|
10
|
+
if x[PNG_HDR]
|
11
|
+
# raw image data
|
12
|
+
@data = x
|
13
|
+
else
|
14
|
+
# filename
|
15
|
+
@data = File.binread(x)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
d = data[0,PNG_HDR.size]
|
20
|
+
if d != PNG_HDR
|
21
|
+
puts "[!] first #{PNG_HDR.size} bytes must be #{PNG_HDR.inspect}, but got #{d.inspect}".red
|
22
|
+
end
|
23
|
+
|
24
|
+
io = StringIO.new(data)
|
25
|
+
io.seek PNG_HDR.size
|
26
|
+
@chunks = []
|
27
|
+
while !io.eof?
|
28
|
+
chunk = Chunk.from_stream(io)
|
29
|
+
@chunks << chunk
|
30
|
+
case chunk
|
31
|
+
when Chunk::IHDR
|
32
|
+
@header = chunk
|
33
|
+
when Chunk::PLTE
|
34
|
+
@palette = chunk
|
35
|
+
when Chunk::IEND
|
36
|
+
break
|
37
|
+
end
|
38
|
+
end
|
39
|
+
unless io.eof?
|
40
|
+
offset = io.tell
|
41
|
+
extradata = io.read
|
42
|
+
puts "[?] #{extradata.size} bytes of extra data after image end (IEND), offset = 0x#{offset.to_s(16)}".red
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def dump
|
47
|
+
@chunks.each do |chunk|
|
48
|
+
puts "[.] #{chunk.inspect} #{chunk.crc_ok? ? 'CRC OK'.green : 'CRC ERROR'.red}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def width
|
53
|
+
@header && @header.width
|
54
|
+
end
|
55
|
+
|
56
|
+
def height
|
57
|
+
@header && @header.height
|
58
|
+
end
|
59
|
+
|
60
|
+
def imagedata
|
61
|
+
if @header
|
62
|
+
raise "only non-interlaced mode is supported for imagedata" if @header.interlace != 0
|
63
|
+
else
|
64
|
+
puts "[?] no image header, assuming non-interlaced RGB".yellow
|
65
|
+
end
|
66
|
+
@imagedata ||= Zlib::Inflate.inflate(@chunks.find_all{ |c| c.type == "IDAT" }.map(&:data).join)
|
67
|
+
end
|
68
|
+
|
69
|
+
def [] x, y
|
70
|
+
scanlines[y][x]
|
71
|
+
end
|
72
|
+
|
73
|
+
def []= x, y, newpixel
|
74
|
+
# we must decode all scanlines before doing any modifications
|
75
|
+
# or scanlines decoded AFTER modification of UPPER ones will be decoded wrong
|
76
|
+
_decode_all_scanlines unless @_all_scanlines_decoded
|
77
|
+
scanlines[y][x] = newpixel
|
78
|
+
end
|
79
|
+
|
80
|
+
def _decode_all_scanlines
|
81
|
+
scanlines.each(&:decode!)
|
82
|
+
@_all_scanlines_decoded = true
|
83
|
+
end
|
84
|
+
|
85
|
+
def scanlines
|
86
|
+
@scanlines ||=
|
87
|
+
begin
|
88
|
+
r = []
|
89
|
+
height.times do |i|
|
90
|
+
r << ScanLine.new(self,i)
|
91
|
+
end
|
92
|
+
r
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def to_s h={}
|
97
|
+
scanlines.map{ |l| l.to_s(h) }.join("\n")
|
98
|
+
end
|
99
|
+
|
100
|
+
def extract_block x,y=nil,w=nil,h=nil
|
101
|
+
if x.is_a?(Hash)
|
102
|
+
Block.new(self,x[:x], x[:y], x[:width], x[:height])
|
103
|
+
else
|
104
|
+
Block.new(self,x,y,w,h)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def each_block bw,bh, &block
|
109
|
+
0.upto(height/bh-1) do |by|
|
110
|
+
0.upto(width/bw-1) do |bx|
|
111
|
+
b = extract_block(bx*bw, by*bh, bw, bh)
|
112
|
+
yield b
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def export
|
118
|
+
imagedata # fill @imagedata, if not already filled
|
119
|
+
|
120
|
+
# delete redundant IDAT chunks
|
121
|
+
first_idat = @chunks.find{ |c| c.type == 'IDAT' }
|
122
|
+
@chunks.delete_if{ |c| c.type == 'IDAT' && c != first_idat }
|
123
|
+
|
124
|
+
# fill first_idat @data with compressed imagedata
|
125
|
+
first_idat.data = Zlib::Deflate.deflate(scanlines.map(&:export).join, 9)
|
126
|
+
|
127
|
+
PNG_HDR + @chunks.map(&:export).join
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
module ZPNG
|
2
|
+
class ScanLine
|
3
|
+
FILTER_NONE = 0
|
4
|
+
FILTER_SUB = 1
|
5
|
+
FILTER_UP = 2
|
6
|
+
FILTER_AVERAGE = 3
|
7
|
+
FILTER_PAETH = 4
|
8
|
+
|
9
|
+
attr_accessor :image, :idx, :filter, :offset
|
10
|
+
|
11
|
+
def initialize image, idx
|
12
|
+
@image,@idx = image,idx
|
13
|
+
@bpp = image.hdr.bpp
|
14
|
+
raise "[!] zero bpp" if @bpp == 0
|
15
|
+
if @BPP = (@bpp%8 == 0) && (@bpp>>3)
|
16
|
+
@offset = idx*(image.width*@BPP+1)
|
17
|
+
else
|
18
|
+
@offset = idx*(image.width*@bpp/8.0+1).ceil
|
19
|
+
end
|
20
|
+
@filter = image.imagedata[@offset].ord
|
21
|
+
@offset += 1
|
22
|
+
end
|
23
|
+
|
24
|
+
def inspect
|
25
|
+
"#<ZPNG::ScanLine " + (instance_variables-[:@image, :@decoded]).
|
26
|
+
map{ |var| "#{var.to_s.tr('@','')}=#{instance_variable_get(var)}" }.
|
27
|
+
join(", ") + ">"
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s h={}
|
31
|
+
white = h[:white] || ' '
|
32
|
+
black = h[:black] || '#'
|
33
|
+
unknown = h[:unknown] || '?'
|
34
|
+
|
35
|
+
@image.width.times.map do |i|
|
36
|
+
px = decode_pixel(i)
|
37
|
+
px.white?? white : (px.black?? black : unknown)
|
38
|
+
end.join
|
39
|
+
end
|
40
|
+
|
41
|
+
def [] x
|
42
|
+
decode_pixel(x)
|
43
|
+
end
|
44
|
+
|
45
|
+
def []= x, newcolor
|
46
|
+
case @bpp
|
47
|
+
when 1
|
48
|
+
flag =
|
49
|
+
if image.hdr.palette_used?
|
50
|
+
idx = image.palette.index(newcolor)
|
51
|
+
raise "no color #{newcolor.inspect} in palette" unless idx
|
52
|
+
idx == 1
|
53
|
+
else
|
54
|
+
if newcolor.white?
|
55
|
+
true
|
56
|
+
elsif newcolor.black?
|
57
|
+
false
|
58
|
+
else
|
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
|
69
|
+
when 8
|
70
|
+
if image.hdr.palette_used?
|
71
|
+
decoded_bytes[x] = (image.palette.index(newcolor)).chr
|
72
|
+
else
|
73
|
+
decoded_bytes[x] = ((newcolor.r + newcolor.g + newcolor.b)/3).chr
|
74
|
+
end
|
75
|
+
when 16
|
76
|
+
if image.hdr.palette_used? && image.hdr.alpha_used?
|
77
|
+
decoded_bytes[x*2] = (image.palette.index(newcolor)).chr
|
78
|
+
decoded_bytes[x*2+1] = (newcolor.alpha || 0xff).chr
|
79
|
+
elsif image.hdr.grayscale? && image.hdr.alpha_used?
|
80
|
+
decoded_bytes[x*2] = newcolor.to_grayscale.chr
|
81
|
+
decoded_bytes[x*2+1] = (newcolor.alpha || 0xff).chr
|
82
|
+
else
|
83
|
+
raise "unexpected colormode #{image.hdr.inspect}"
|
84
|
+
end
|
85
|
+
when 24
|
86
|
+
decoded_bytes[x*3,3] = [newcolor.r, newcolor.g, newcolor.b].map(&:chr).join
|
87
|
+
when 32
|
88
|
+
decoded_bytes[x*4,4] = [newcolor.r, newcolor.g, newcolor.b, newcolor.a].map(&:chr).join
|
89
|
+
else raise "unsupported bpp #{@bpp}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def decode_pixel x
|
94
|
+
raw =
|
95
|
+
if @BPP
|
96
|
+
# 8, 16 or 32 bits per pixel
|
97
|
+
decoded_bytes[x*@BPP, @BPP]
|
98
|
+
else
|
99
|
+
# 1, 2 or 4 bits per pixel
|
100
|
+
decoded_bytes[x*@bpp/8, (@bpp/8.0).ceil]
|
101
|
+
end
|
102
|
+
|
103
|
+
r = g = b = a = nil
|
104
|
+
|
105
|
+
colormode = image.hdr.color
|
106
|
+
|
107
|
+
if image.hdr.palette_used?
|
108
|
+
idx =
|
109
|
+
case @bpp
|
110
|
+
when 1
|
111
|
+
# needed for palette
|
112
|
+
(raw.ord & (1<<(7-(x%8)))) == 0 ? 0 : 1
|
113
|
+
when 8
|
114
|
+
raw.ord
|
115
|
+
when 16
|
116
|
+
raw[0].ord
|
117
|
+
else raise "unexpected bpp #{@bpp}"
|
118
|
+
end
|
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
|
129
|
+
else
|
130
|
+
raise "unexpected colormode #{colormode} for bpp #{@bpp}"
|
131
|
+
end
|
132
|
+
when 16
|
133
|
+
if colormode == ZPNG::Chunk::IHDR::COLOR_GRAY_ALPHA
|
134
|
+
r=g=b= raw[0].ord
|
135
|
+
a = raw[1].ord
|
136
|
+
else
|
137
|
+
raise "unexpected colormode #{colormode} for bpp #{@bpp}"
|
138
|
+
end
|
139
|
+
when 24
|
140
|
+
r,g,b = raw.split('').map(&:ord)
|
141
|
+
when 32
|
142
|
+
r,g,b,a = raw.split('').map(&:ord)
|
143
|
+
else raise "unexpected bpp #{@bpp}"
|
144
|
+
end
|
145
|
+
|
146
|
+
Color.new(r,g,b,a)
|
147
|
+
end
|
148
|
+
|
149
|
+
def decoded_bytes
|
150
|
+
@decoded_bytes ||=
|
151
|
+
begin
|
152
|
+
# number of bytes per complete pixel, rounding up to one
|
153
|
+
bpp1 = (@bpp/8.0).ceil
|
154
|
+
|
155
|
+
# bytes in one scanline
|
156
|
+
nbytes = (image.width*@bpp/8.0).ceil
|
157
|
+
|
158
|
+
s = ''
|
159
|
+
nbytes.times do |i|
|
160
|
+
b0 = (i-bpp1) >= 0 ? s[i-bpp1] : nil
|
161
|
+
s[i] = decode_byte(i, b0, bpp1)
|
162
|
+
end
|
163
|
+
# print Hexdump.dump(s[0,16])
|
164
|
+
s
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def decode!
|
169
|
+
decoded_bytes
|
170
|
+
true
|
171
|
+
end
|
172
|
+
|
173
|
+
def decode_byte x, b0, bpp1
|
174
|
+
raw = @image.imagedata[@offset+x]
|
175
|
+
|
176
|
+
unless raw
|
177
|
+
STDERR.puts "[!] not enough bytes at pos #{x} of scanline #@idx".red
|
178
|
+
raw = 0.chr
|
179
|
+
end
|
180
|
+
|
181
|
+
case @filter
|
182
|
+
when FILTER_NONE # 0
|
183
|
+
raw
|
184
|
+
|
185
|
+
when FILTER_SUB # 1
|
186
|
+
return raw unless b0
|
187
|
+
((raw.ord + b0.ord) & 0xff).chr
|
188
|
+
|
189
|
+
when FILTER_UP # 2
|
190
|
+
return raw if @idx == 0
|
191
|
+
prev = @image.scanlines[@idx-1].decoded_bytes[x]
|
192
|
+
((raw.ord + prev.ord) & 0xff).chr
|
193
|
+
|
194
|
+
when FILTER_AVERAGE # 3
|
195
|
+
prev = (b0 && b0.ord) || 0
|
196
|
+
prior = (@idx > 0) ? @image.scanlines[@idx-1].decoded_bytes[x].ord : 0
|
197
|
+
((raw.ord + (prev + prior)/2) & 0xff).chr
|
198
|
+
|
199
|
+
when FILTER_PAETH # 4
|
200
|
+
pa = (b0 && b0.ord) || 0
|
201
|
+
pb = (@idx > 0) ? @image.scanlines[@idx-1].decoded_bytes[x].ord : 0
|
202
|
+
pc = (b0 && @idx > 0) ? @image.scanlines[@idx-1].decoded_bytes[x-bpp1].ord : 0
|
203
|
+
((raw.ord + paeth_predictor(pa, pb, pc)) & 0xff).chr
|
204
|
+
else
|
205
|
+
raise "invalid ScanLine filter #{@filter}"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def paeth_predictor a,b,c
|
210
|
+
p = a + b - c
|
211
|
+
pa = (p - a).abs
|
212
|
+
pb = (p - b).abs
|
213
|
+
pc = (p - c).abs
|
214
|
+
(pa <= pb) ? (pa <= pc ? a : c) : (pb <= pc ? b : c)
|
215
|
+
end
|
216
|
+
|
217
|
+
def export
|
218
|
+
# we export in FILTER_NONE mode
|
219
|
+
"\x00" + decoded_bytes
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|