zpng 0.0.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/.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
|