zpng 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/lib/zpng/color.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
module ZPNG
|
2
2
|
class Color
|
3
|
-
attr_accessor :r, :g, :b
|
4
|
-
|
3
|
+
attr_accessor :r, :g, :b
|
4
|
+
attr_reader :a
|
5
|
+
attr_accessor :depth
|
5
6
|
|
6
7
|
include DeepCopyable
|
7
8
|
|
@@ -9,16 +10,18 @@ module ZPNG
|
|
9
10
|
h = a.last.is_a?(Hash) ? a.pop : {}
|
10
11
|
@r,@g,@b,@a = *a
|
11
12
|
|
12
|
-
# default ALPHA = 0xff - opaque
|
13
|
-
@a ||= h[:alpha] || 0xff
|
14
|
-
|
15
13
|
# default sample depth for r,g,b and alpha = 8 bits
|
16
14
|
@depth = h[:depth] || 8
|
17
|
-
|
15
|
+
|
16
|
+
# default ALPHA = 0xff - opaque
|
17
|
+
@a ||= h[:alpha] || (2**@depth-1)
|
18
18
|
end
|
19
19
|
|
20
|
-
|
21
|
-
|
20
|
+
def a= a
|
21
|
+
@a = a || (2**@depth-1) # NULL alpha means fully opaque
|
22
|
+
end
|
23
|
+
alias :alpha :a
|
24
|
+
alias :alpha= :a=
|
22
25
|
|
23
26
|
BLACK = Color.new(0 , 0, 0)
|
24
27
|
WHITE = Color.new(255,255,255)
|
@@ -32,6 +35,8 @@ module ZPNG
|
|
32
35
|
PURPLE= MAGENTA =
|
33
36
|
Color.new(255, 0,255)
|
34
37
|
|
38
|
+
TRANSPARENT = Color.new(0,0,0,0)
|
39
|
+
|
35
40
|
ANSI_COLORS = [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white]
|
36
41
|
|
37
42
|
#ASCII_MAP = %q_ .`,-:;~"!<+*^(LJ=?vctsxj12FuoCeyPSah5wVmXA4G9$OR0MQNW#&%@_
|
@@ -51,6 +56,7 @@ module ZPNG
|
|
51
56
|
|
52
57
|
# euclidian distance - http://en.wikipedia.org/wiki/Euclidean_distance
|
53
58
|
def euclidian other_color
|
59
|
+
# TODO: different depths
|
54
60
|
r = (self.r.to_i - other_color.r.to_i)**2
|
55
61
|
r += (self.g.to_i - other_color.g.to_i)**2
|
56
62
|
r += (self.b.to_i - other_color.b.to_i)**2
|
@@ -58,7 +64,8 @@ module ZPNG
|
|
58
64
|
end
|
59
65
|
|
60
66
|
def white?
|
61
|
-
|
67
|
+
max = 2**depth-1
|
68
|
+
r == max && g == max && b == max
|
62
69
|
end
|
63
70
|
|
64
71
|
def black?
|
@@ -69,26 +76,51 @@ module ZPNG
|
|
69
76
|
a == 0
|
70
77
|
end
|
71
78
|
|
79
|
+
def opaque?
|
80
|
+
a.nil? || a == 2**depth-1
|
81
|
+
end
|
82
|
+
|
72
83
|
def to_grayscale
|
73
84
|
(r+g+b)/3
|
74
85
|
end
|
75
86
|
|
76
|
-
def
|
77
|
-
|
87
|
+
def to_gray_alpha
|
88
|
+
[to_grayscale, alpha]
|
89
|
+
end
|
90
|
+
|
91
|
+
# from_grayscale level
|
92
|
+
# from_grayscale level, :depth => 16
|
93
|
+
# from_grayscale level, alpha
|
94
|
+
# from_grayscale level, alpha, :depth => 16
|
95
|
+
def self.from_grayscale value, *args
|
96
|
+
Color.new value,value,value, *args
|
97
|
+
end
|
98
|
+
|
99
|
+
########################################################
|
100
|
+
# simple conversions
|
101
|
+
|
102
|
+
def to_i
|
103
|
+
((a||0) << 24) + ((r||0) << 16) + ((g||0) << 8) + (b||0)
|
78
104
|
end
|
79
105
|
|
80
106
|
def to_s
|
81
107
|
"%02X%02X%02X" % [r,g,b]
|
82
108
|
end
|
83
109
|
|
110
|
+
def to_a
|
111
|
+
[r, g, b, a]
|
112
|
+
end
|
113
|
+
|
84
114
|
########################################################
|
115
|
+
# complex conversions
|
85
116
|
|
86
|
-
# try to convert to pseudographics
|
117
|
+
# try to convert to one pseudographics ASCII character
|
87
118
|
def to_ascii map=ASCII_MAP
|
88
119
|
#p self
|
89
120
|
map[self.to_grayscale*(map.size-1)/(2**@depth-1), 1]
|
90
121
|
end
|
91
122
|
|
123
|
+
# convert to ANSI color name
|
92
124
|
def to_ansi
|
93
125
|
return to_depth(8).to_ansi if depth != 8
|
94
126
|
a = ANSI_COLORS.map{|c| self.class.const_get(c.to_s.upcase) }
|
@@ -96,6 +128,7 @@ module ZPNG
|
|
96
128
|
ANSI_COLORS[a.index(a.min)]
|
97
129
|
end
|
98
130
|
|
131
|
+
# HTML/CSS color in notation like #33aa88
|
99
132
|
def to_css
|
100
133
|
return to_depth(8).to_css if depth != 8
|
101
134
|
"#%02X%02X%02X" % [r,g,b]
|
@@ -104,30 +137,22 @@ module ZPNG
|
|
104
137
|
|
105
138
|
########################################################
|
106
139
|
|
107
|
-
def to_i
|
108
|
-
((a||0) << 24) + ((r||0) << 16) + ((g||0) << 8) + (b||0)
|
109
|
-
end
|
110
|
-
|
111
140
|
# change bit depth, return new Color
|
112
141
|
def to_depth new_depth
|
113
|
-
|
142
|
+
return self if depth == new_depth
|
143
|
+
|
144
|
+
color = Color.new :depth => new_depth
|
114
145
|
if new_depth > self.depth
|
115
|
-
%w'r g b'.each do |part|
|
116
|
-
color
|
117
|
-
if color%2 == 0
|
118
|
-
color <<= (new_depth-self.depth)
|
119
|
-
else
|
120
|
-
(new_depth-self.depth).times{ color = color*2 + 1 }
|
121
|
-
end
|
122
|
-
c.send("#{part}=", color)
|
146
|
+
%w'r g b a'.each do |part|
|
147
|
+
color.send("#{part}=", (2**new_depth-1)/(2**depth-1)*self.send(part))
|
123
148
|
end
|
124
149
|
else
|
125
150
|
# new_depth < self.depth
|
126
|
-
%w'r g b'.each do |part|
|
127
|
-
|
151
|
+
%w'r g b a'.each do |part|
|
152
|
+
color.send("#{part}=", self.send(part)>>(self.depth-new_depth))
|
128
153
|
end
|
129
154
|
end
|
130
|
-
|
155
|
+
color
|
131
156
|
end
|
132
157
|
|
133
158
|
def inspect
|
@@ -136,16 +161,46 @@ module ZPNG
|
|
136
161
|
s << " r=" + (r ? "%04x" % r : "????")
|
137
162
|
s << " g=" + (g ? "%04x" % g : "????")
|
138
163
|
s << " b=" + (b ? "%04x" % b : "????")
|
164
|
+
s << " alpha=%04x" % alpha if alpha && alpha != 0xffff
|
139
165
|
else
|
140
166
|
s << " #"
|
141
167
|
s << (r ? "%02x" % r : "??")
|
142
168
|
s << (g ? "%02x" % g : "??")
|
143
169
|
s << (b ? "%02x" % b : "??")
|
170
|
+
s << " alpha=%02x" % alpha if alpha && alpha != 0xff
|
144
171
|
end
|
145
|
-
s << " a=#{a}" if a && alpha_depth != 0
|
146
172
|
s << " depth=#{depth}" if depth != 8
|
147
|
-
s << " alpha_depth=#{alpha_depth}" if alpha_depth != 8 && alpha_depth != 0
|
148
173
|
s << ">"
|
149
174
|
end
|
175
|
+
|
176
|
+
# compare with other color
|
177
|
+
def == c
|
178
|
+
return false unless c.is_a?(Color)
|
179
|
+
c1,c2 =
|
180
|
+
if self.depth > c.depth
|
181
|
+
[self, c.to_depth(self.depth)]
|
182
|
+
else
|
183
|
+
[self.to_depth(c.depth), c]
|
184
|
+
end
|
185
|
+
c1.r == c2.r && c1.g == c2.g && c1.b == c2.b && c1.a == c2.a
|
186
|
+
end
|
187
|
+
alias :eql? :==
|
188
|
+
|
189
|
+
# compare with other color
|
190
|
+
def <=> c
|
191
|
+
c1,c2 =
|
192
|
+
if self.depth > c.depth
|
193
|
+
[self, c.to_depth(self.depth)]
|
194
|
+
else
|
195
|
+
[self.to_depth(c.depth), c]
|
196
|
+
end
|
197
|
+
r = c1.to_grayscale <=> c2.to_grayscale
|
198
|
+
r == 0 ? (c1.to_a <=> c2.to_a) : r
|
199
|
+
end
|
200
|
+
|
201
|
+
# for Array.uniq()
|
202
|
+
def hash
|
203
|
+
self.to_i
|
204
|
+
end
|
150
205
|
end
|
151
206
|
end
|
data/lib/zpng/hexdump.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module ZPNG
|
4
|
+
module Hexdump
|
5
|
+
|
6
|
+
def hexdump *args, &block
|
7
|
+
print Hexdump.dump(*args, &block)
|
8
|
+
end
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def dump data, h = {}
|
12
|
+
offset = h[:offset] || 0
|
13
|
+
add = h[:add] || 0
|
14
|
+
size = h[:size] || (data.size-offset)
|
15
|
+
tail = h[:tail] || "\n"
|
16
|
+
width = h[:width] || 0x10 # row width, in bytes
|
17
|
+
|
18
|
+
h[:show_offset] = true unless h.key?(:show_offset)
|
19
|
+
h[:dedup] = true unless h.key?(:dedup)
|
20
|
+
|
21
|
+
size = data.size-offset if size+offset > data.size
|
22
|
+
|
23
|
+
r = ''; prevhex = ''; c = nil; prevdup = false
|
24
|
+
while true
|
25
|
+
ascii = ''; hex = ''
|
26
|
+
width.times do |i|
|
27
|
+
hex << ' ' if i%8==0 && i>0
|
28
|
+
if c = ((size > 0) && data[offset+i])
|
29
|
+
hex << "%02x " % c.ord
|
30
|
+
ascii << ((32..126).include?(c.ord) ? c : '.')
|
31
|
+
else
|
32
|
+
hex << ' '
|
33
|
+
ascii << ' '
|
34
|
+
end
|
35
|
+
size-=1
|
36
|
+
end
|
37
|
+
|
38
|
+
if h[:dedup] && hex == prevhex
|
39
|
+
row = "*"
|
40
|
+
yield(row, offset+add, ascii) if block_given?
|
41
|
+
r << row << "\n" unless prevdup
|
42
|
+
prevdup = true
|
43
|
+
else
|
44
|
+
row = (h[:show_offset] ? ("%08x: " % (offset + add)) : '') + hex + ' |' + ascii + "|"
|
45
|
+
yield(row, offset+add, ascii) if block_given?
|
46
|
+
r << row << "\n"
|
47
|
+
prevdup = false
|
48
|
+
end
|
49
|
+
|
50
|
+
offset += width
|
51
|
+
prevhex = hex
|
52
|
+
break if size <= 0
|
53
|
+
end
|
54
|
+
if h[:show_offset] && prevdup
|
55
|
+
row = "%08x: " % (offset + add)
|
56
|
+
yield(row) if block_given?
|
57
|
+
r << row
|
58
|
+
end
|
59
|
+
r.chomp + tail
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
if $0 == __FILE__
|
66
|
+
h = {}
|
67
|
+
case ARGV.size
|
68
|
+
when 0
|
69
|
+
puts "gimme fname [offset] [size]"
|
70
|
+
exit
|
71
|
+
when 1
|
72
|
+
fname = ARGV[0]
|
73
|
+
when 2
|
74
|
+
fname = ARGV[0]
|
75
|
+
h[:offset] = ARGV[1].to_i
|
76
|
+
when 3
|
77
|
+
fname = ARGV[0]
|
78
|
+
h[:offset] = ARGV[1].to_i
|
79
|
+
h[:size] = ARGV[2].to_i
|
80
|
+
end
|
81
|
+
File.open(fname,"rb") do |f|
|
82
|
+
f.seek h[:offset] if h[:offset]
|
83
|
+
@data = f.read(h[:size])
|
84
|
+
end
|
85
|
+
puts ZPNG::Hexdump.dump(@data)
|
86
|
+
end
|
data/lib/zpng/image.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module ZPNG
|
2
2
|
class Image
|
3
|
-
attr_accessor :data, :header, :chunks, :scanlines, :imagedata
|
3
|
+
attr_accessor :data, :header, :chunks, :scanlines, :imagedata
|
4
4
|
alias :hdr :header
|
5
5
|
|
6
6
|
include DeepCopyable
|
@@ -28,13 +28,16 @@ module ZPNG
|
|
28
28
|
@adam7 ||= Adam7Decoder.new(self)
|
29
29
|
end
|
30
30
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
31
|
+
class << self
|
32
|
+
# load image from file
|
33
|
+
def load fname
|
34
|
+
open(fname,"rb") do |f|
|
35
|
+
self.new(f)
|
36
|
+
end
|
35
37
|
end
|
38
|
+
alias :load_file :load
|
39
|
+
alias :from_file :load # as in ChunkyPNG
|
36
40
|
end
|
37
|
-
alias :load_file :load
|
38
41
|
|
39
42
|
# save image to file
|
40
43
|
def save fname
|
@@ -52,10 +55,19 @@ module ZPNG
|
|
52
55
|
|
53
56
|
def _from_hash h
|
54
57
|
@new_image = true
|
55
|
-
@chunks << (@header
|
58
|
+
@chunks << (@header = Chunk::IHDR.new(h))
|
56
59
|
if @header.palette_used?
|
57
|
-
|
58
|
-
|
60
|
+
if h.key?(:palette)
|
61
|
+
if h[:palette]
|
62
|
+
@chunks << h[:palette]
|
63
|
+
else
|
64
|
+
# :palette => nil
|
65
|
+
# assume palette will be added later
|
66
|
+
end
|
67
|
+
else
|
68
|
+
@chunks << Chunk::PLTE.new
|
69
|
+
palette[0] = h[:background] || h[:bg] || Color::BLACK # add default bg color
|
70
|
+
end
|
59
71
|
end
|
60
72
|
end
|
61
73
|
|
@@ -86,8 +98,6 @@ module ZPNG
|
|
86
98
|
case chunk
|
87
99
|
when Chunk::IHDR
|
88
100
|
@header = chunk
|
89
|
-
when Chunk::PLTE
|
90
|
-
@palette = chunk
|
91
101
|
when Chunk::IEND
|
92
102
|
break
|
93
103
|
end
|
@@ -100,12 +110,58 @@ module ZPNG
|
|
100
110
|
end
|
101
111
|
|
102
112
|
public
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
113
|
+
|
114
|
+
# internal helper method for color types 0 (grayscale) and 2 (truecolor)
|
115
|
+
def _alpha_color color
|
116
|
+
return nil unless trns
|
117
|
+
|
118
|
+
# For color type 0 (grayscale), the tRNS chunk contains a single gray level value, stored in the format:
|
119
|
+
#
|
120
|
+
# Gray: 2 bytes, range 0 .. (2^bitdepth)-1
|
121
|
+
#
|
122
|
+
# For color type 2 (truecolor), the tRNS chunk contains a single RGB color value, stored in the format:
|
123
|
+
#
|
124
|
+
# Red: 2 bytes, range 0 .. (2^bitdepth)-1
|
125
|
+
# Green: 2 bytes, range 0 .. (2^bitdepth)-1
|
126
|
+
# Blue: 2 bytes, range 0 .. (2^bitdepth)-1
|
127
|
+
#
|
128
|
+
# (If the image bit depth is less than 16, the least significant bits are used and the others are 0)
|
129
|
+
# Pixels of the specified gray level are to be treated as transparent (equivalent to alpha value 0);
|
130
|
+
# all other pixels are to be treated as fully opaque ( alpha = (2^bitdepth)-1 )
|
131
|
+
|
132
|
+
@alpha_color ||=
|
133
|
+
case hdr.color
|
134
|
+
when COLOR_GRAYSCALE
|
135
|
+
v = trns.data.unpack('n')[0] & (2**hdr.depth-1)
|
136
|
+
Color.from_grayscale(v, :depth => hdr.depth)
|
137
|
+
when COLOR_RGB
|
138
|
+
a = trns.data.unpack('n3').map{ |v| v & (2**hdr.depth-1) }
|
139
|
+
Color.new(*a, :depth => hdr.depth)
|
140
|
+
else
|
141
|
+
raise "color2alpha only intended for GRAYSCALE & RGB color modes"
|
142
|
+
end
|
143
|
+
|
144
|
+
color == @alpha_color ? 0 : (2**hdr.depth-1)
|
145
|
+
end
|
146
|
+
|
147
|
+
public
|
148
|
+
|
149
|
+
###########################################################################
|
150
|
+
# chunks access
|
151
|
+
|
152
|
+
def trns
|
153
|
+
# not used "@trns ||= ..." here b/c it will call find() each time of there's no TRNS chunk
|
154
|
+
defined?(@trns) ? @trns : (@trns=@chunks.find{ |c| c.is_a?(Chunk::TRNS) })
|
107
155
|
end
|
108
156
|
|
157
|
+
def plte
|
158
|
+
@plte ||= @chunks.find{ |c| c.is_a?(Chunk::PLTE) }
|
159
|
+
end
|
160
|
+
alias :palette :plte
|
161
|
+
|
162
|
+
###########################################################################
|
163
|
+
# image attributes
|
164
|
+
|
109
165
|
def bpp
|
110
166
|
@header && @header.bpp
|
111
167
|
end
|
@@ -139,6 +195,10 @@ module ZPNG
|
|
139
195
|
end
|
140
196
|
end
|
141
197
|
|
198
|
+
def metadata
|
199
|
+
@metadata ||= Metadata.new(self)
|
200
|
+
end
|
201
|
+
|
142
202
|
def [] x, y
|
143
203
|
x,y = adam7.convert_coords(x,y) if interlaced?
|
144
204
|
scanlines[y][x]
|
@@ -153,7 +213,7 @@ module ZPNG
|
|
153
213
|
# we must decode all scanlines before doing any modifications
|
154
214
|
# or scanlines decoded AFTER modification of UPPER ones will be decoded wrong
|
155
215
|
def decode_all_scanlines
|
156
|
-
return if @all_scanlines_decoded
|
216
|
+
return if @all_scanlines_decoded || new_image?
|
157
217
|
@all_scanlines_decoded = true
|
158
218
|
scanlines.each(&:decode!)
|
159
219
|
end
|
@@ -201,7 +261,8 @@ module ZPNG
|
|
201
261
|
end
|
202
262
|
|
203
263
|
def export
|
204
|
-
|
264
|
+
# fill @imagedata, if not already filled
|
265
|
+
imagedata unless new_image?
|
205
266
|
|
206
267
|
# delete old IDAT chunks
|
207
268
|
@chunks.delete_if{ |c| c.is_a?(Chunk::IDAT) }
|
@@ -255,6 +316,16 @@ module ZPNG
|
|
255
316
|
deep_copy.crop!(params)
|
256
317
|
end
|
257
318
|
|
319
|
+
def pixels
|
320
|
+
Pixels.new(self)
|
321
|
+
end
|
322
|
+
|
323
|
+
def == other_image
|
324
|
+
width == other_image.width &&
|
325
|
+
height == other_image.height &&
|
326
|
+
pixels == other_image.pixels
|
327
|
+
end
|
328
|
+
|
258
329
|
def each_pixel &block
|
259
330
|
height.times do |y|
|
260
331
|
width.times do |x|
|
@@ -267,22 +338,29 @@ module ZPNG
|
|
267
338
|
# OR returns self if no need to deinterlace
|
268
339
|
def deinterlace
|
269
340
|
return self unless interlaced?
|
270
|
-
require 'pp'
|
271
|
-
pp chunks
|
272
341
|
|
273
342
|
# copy all but 'interlace' header params
|
274
343
|
h = Hash[*%w'width height depth color compression filter'.map{ |k| [k.to_sym, hdr.send(k)] }.flatten]
|
275
|
-
|
344
|
+
|
345
|
+
# don't auto-add palette chunk
|
346
|
+
h[:palette] = nil
|
347
|
+
|
348
|
+
# create new img
|
349
|
+
new_img = self.class.new h
|
350
|
+
|
351
|
+
# copy all but hdr/imagedata/end chunks
|
276
352
|
chunks.each do |chunk|
|
277
353
|
next if chunk.is_a?(Chunk::IHDR)
|
278
354
|
next if chunk.is_a?(Chunk::IDAT)
|
279
355
|
next if chunk.is_a?(Chunk::IEND)
|
280
356
|
new_img.chunks << chunk.deep_copy
|
281
357
|
end
|
358
|
+
|
359
|
+
# pixel-by-pixel copy
|
282
360
|
each_pixel do |c,x,y|
|
283
361
|
new_img[x,y] = c
|
284
362
|
end
|
285
|
-
|
363
|
+
|
286
364
|
new_img
|
287
365
|
end
|
288
366
|
end
|