zpng 0.4.4 → 0.4.6
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.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/Gemfile.lock +75 -30
- data/VERSION +1 -1
- data/lib/zpng/bmp/reader.rb +12 -7
- data/lib/zpng/cli.rb +2 -2
- data/lib/zpng/color.rb +92 -18
- data/lib/zpng/image.rb +182 -14
- data/lib/zpng/jpeg/chunks.rb +164 -0
- data/lib/zpng/jpeg/reader.rb +55 -0
- data/lib/zpng/scan_line.rb +38 -5
- data/lib/zpng.rb +2 -1
- data/spec/bad_samples_spec.rb +12 -0
- data/spec/cli_spec.rb +0 -2
- data/spec/image_spec.rb +99 -24
- data/spec/pixel_access_spec.rb +34 -0
- data/spec/rotate_spec.rb +62 -0
- data/zpng.gemspec +9 -5
- metadata +20 -4
- data/lib/zpng/readable_struct.rb +0 -56
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# -*- coding:binary; frozen_string_literal: true -*-
|
|
2
|
+
|
|
3
|
+
module ZPNG
|
|
4
|
+
module JPEG
|
|
5
|
+
|
|
6
|
+
class Chunk
|
|
7
|
+
attr_accessor :marker, :size, :data
|
|
8
|
+
|
|
9
|
+
def initialize marker, io
|
|
10
|
+
@marker = marker
|
|
11
|
+
@size = io.read(2).unpack('n')[0]
|
|
12
|
+
@data = io.read(@size-2)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def type
|
|
16
|
+
r = self.class.name.split("::").last.ljust(4)
|
|
17
|
+
r = "ch_%02X" % @marker[1].ord if r == "Chunk"
|
|
18
|
+
r
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def crc
|
|
22
|
+
:no_crc
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def inspect *args
|
|
26
|
+
size = @size ? sprintf("%6d",@size) : sprintf("%6s","???")
|
|
27
|
+
sprintf "<%4s size=%s >", type, size
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def export *args
|
|
31
|
+
@marker + [@size].pack('n') + @data
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class APP < Chunk
|
|
36
|
+
attr_accessor :name
|
|
37
|
+
|
|
38
|
+
# BYTE Version[2]; /* 07h JFIF Format Revision */
|
|
39
|
+
# BYTE Units; /* 09h Units used for Resolution */
|
|
40
|
+
# BYTE Xdensity[2]; /* 0Ah Horizontal Resolution */
|
|
41
|
+
# BYTE Ydensity[2]; /* 0Ch Vertical Resolution */
|
|
42
|
+
# BYTE XThumbnail; /* 0Eh Horizontal Pixel Count */
|
|
43
|
+
# BYTE YThumbnail; /* 0Fh Vertical Pixel Count */
|
|
44
|
+
class JFIF < IOStruct.new( 'vCnnCC', :version, :units, :xdensity, :ydensity, :xthumbnail, :ythumbnail )
|
|
45
|
+
def inspect *args
|
|
46
|
+
r = "<" + super.split(' ',3).last
|
|
47
|
+
r.sub!(/version=\d+/, "version=#{version >> 8}.#{version & 0xff}") if version
|
|
48
|
+
r
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def initialize marker, io
|
|
53
|
+
super
|
|
54
|
+
@id = marker[1].ord & 0xf
|
|
55
|
+
@name = @data.unpack('Z*')[0]
|
|
56
|
+
if @name == 'JFIF'
|
|
57
|
+
@jfif = JFIF.read(@data[5..-1])
|
|
58
|
+
# TODO: read thumbnail, see https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def type
|
|
63
|
+
"APP#{@id}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def inspect *args
|
|
67
|
+
r = super.chop + ("name=%s >" % name.inspect)
|
|
68
|
+
if @jfif
|
|
69
|
+
r = r.chop + ("jfif=%s>" % @jfif.inspect)
|
|
70
|
+
end
|
|
71
|
+
r
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class SOF < Chunk
|
|
76
|
+
def initialize marker, io
|
|
77
|
+
super
|
|
78
|
+
@id = marker[1].ord & 0xf
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def type
|
|
82
|
+
"SOF#{@id}"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class SOF0 < SOF
|
|
87
|
+
attr_accessor :bpp, :width, :height, :components
|
|
88
|
+
attr_accessor :color # for compatibility with IHDR
|
|
89
|
+
|
|
90
|
+
def initialize marker, io
|
|
91
|
+
super
|
|
92
|
+
@bpp, @height, @width, @components = @data.unpack('CnnC')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def inspect *args
|
|
96
|
+
super.chop + ("bpp=%d width=%d height=%d components=%d >" % [bpp, width, height, components])
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
class SOF2 < SOF
|
|
101
|
+
attr_accessor :precision, :width, :height, :components
|
|
102
|
+
attr_accessor :color # for compatibility with IHDR
|
|
103
|
+
|
|
104
|
+
def initialize marker, io
|
|
105
|
+
super
|
|
106
|
+
@precision, @height, @width, @components = @data.unpack('CnnC')
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def bpp
|
|
110
|
+
precision
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def inspect *args
|
|
114
|
+
super.chop + ("precision=%d width=%d height=%d components=%d >" % [precision, width, height, components])
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
class DHT < Chunk
|
|
119
|
+
attr_accessor :id, :lengths, :values
|
|
120
|
+
|
|
121
|
+
def initialize marker, io
|
|
122
|
+
super
|
|
123
|
+
@id, *@lengths = @data.unpack("CC16")
|
|
124
|
+
@values = @data.unpack("x17C" + @lengths.inject(:+).to_s)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def inspect verbose = 0
|
|
128
|
+
r = super.chop + ("id=%02x lengths=%s >" % [id, lengths.inspect])
|
|
129
|
+
r = r.chop + ("values=%s >" % [values.inspect]) if verbose > 0
|
|
130
|
+
r
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
class SOS < Chunk; end
|
|
135
|
+
class DRI < Chunk; end
|
|
136
|
+
class DQT < Chunk; end
|
|
137
|
+
class DAC < Chunk; end
|
|
138
|
+
|
|
139
|
+
class COM < Chunk
|
|
140
|
+
def inspect *args
|
|
141
|
+
super.chop + ("data=%s>" % data.inspect)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Its length is unknown in advance, nor defined in the file.
|
|
146
|
+
# The only way to get its length is to either decode it or to fast-forward over it:
|
|
147
|
+
# just scan forward for a FF byte. If it's a restart marker (followed by D0 - D7) or a data FF (followed by 00), continue.
|
|
148
|
+
class ECS < Chunk
|
|
149
|
+
def initialize io
|
|
150
|
+
@data = io.read
|
|
151
|
+
if (pos = @data.index(/\xff[^\x00\xd0-\xd7]/))
|
|
152
|
+
io.seek(pos-@data.size, :CUR) # seek back
|
|
153
|
+
@data = @data[0, pos]
|
|
154
|
+
end
|
|
155
|
+
@size = @data.size
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def export *args
|
|
159
|
+
@data
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# -*- coding:binary; frozen_string_literal: true -*-
|
|
2
|
+
|
|
3
|
+
# https://github.com/corkami/formats/blob/master/image/jpeg.md
|
|
4
|
+
# https://docs.fileformat.com/image/jpeg/
|
|
5
|
+
# https://www.file-recovery.com/jpg-signature-format.htm
|
|
6
|
+
# https://exiftool.org/TagNames/JPEG.html
|
|
7
|
+
|
|
8
|
+
module ZPNG
|
|
9
|
+
module JPEG
|
|
10
|
+
|
|
11
|
+
SOI = "\xff\xd8" # Start of Image
|
|
12
|
+
EOI = "\xff\xd9" # End of Image
|
|
13
|
+
|
|
14
|
+
MAGIC = SOI
|
|
15
|
+
|
|
16
|
+
module Reader
|
|
17
|
+
def _read_jpeg io
|
|
18
|
+
@format = :jpeg
|
|
19
|
+
|
|
20
|
+
while !io.eof?
|
|
21
|
+
marker = io.read(2)
|
|
22
|
+
break if marker == EOI
|
|
23
|
+
|
|
24
|
+
case marker[1].ord
|
|
25
|
+
when 0xc0
|
|
26
|
+
@chunks << (@ihdr=SOF0.new(marker, io))
|
|
27
|
+
when 0xc2
|
|
28
|
+
@chunks << (@ihdr=SOF2.new(marker, io))
|
|
29
|
+
when 0xc4
|
|
30
|
+
@chunks << DHT.new(marker, io)
|
|
31
|
+
when 0xcc
|
|
32
|
+
@chunks << DAC.new(marker, io)
|
|
33
|
+
when 0xc1..0xcf
|
|
34
|
+
@chunks << SOF.new(marker, io)
|
|
35
|
+
when 0xda
|
|
36
|
+
@chunks << SOS.new(marker, io)
|
|
37
|
+
# Entropy-Coded Segment starts
|
|
38
|
+
@chunks << ECS.new(io)
|
|
39
|
+
when 0xdb
|
|
40
|
+
@chunks << DQT.new(marker, io)
|
|
41
|
+
when 0xdd
|
|
42
|
+
@chunks << DRI.new(marker, io)
|
|
43
|
+
when 0xe0..0xef
|
|
44
|
+
@chunks << APP.new(marker, io)
|
|
45
|
+
when 0xfe
|
|
46
|
+
@chunks << COM.new(marker, io)
|
|
47
|
+
else
|
|
48
|
+
$stderr.puts "[?] Unknown JPEG marker #{marker.inspect}".yellow
|
|
49
|
+
@chunks << Chunk.new(marker, io)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/zpng/scan_line.rb
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#coding: binary
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
2
5
|
module ZPNG
|
|
3
6
|
class ScanLine
|
|
4
7
|
FILTER_NONE = 0
|
|
@@ -39,6 +42,7 @@ module ZPNG
|
|
|
39
42
|
STDERR.puts "[!] #{self.class}: ##@idx: no data at pos 0, scanline dropped".red
|
|
40
43
|
end
|
|
41
44
|
end
|
|
45
|
+
@errors = Set.new
|
|
42
46
|
end
|
|
43
47
|
|
|
44
48
|
# ScanLine is BAD if it has no filter
|
|
@@ -146,6 +150,24 @@ module ZPNG
|
|
|
146
150
|
end # case image.hdr.color
|
|
147
151
|
end
|
|
148
152
|
|
|
153
|
+
def get_raw x
|
|
154
|
+
return nil if @bpp > 8 || image.hdr.color != COLOR_INDEXED
|
|
155
|
+
|
|
156
|
+
raw =
|
|
157
|
+
if @BPP
|
|
158
|
+
# 8, 16, 24, 32, 48 bits per pixel
|
|
159
|
+
decoded_bytes[x*@BPP, @BPP]
|
|
160
|
+
else
|
|
161
|
+
# 1, 2 or 4 bits per pixel
|
|
162
|
+
decoded_bytes[x*@bpp/8]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
mask = 2**@bpp-1
|
|
166
|
+
shift = 8-(x%(8/@bpp)+1)*@bpp
|
|
167
|
+
raise "invalid shift #{shift}" if shift < 0 || shift > 7
|
|
168
|
+
idx = (raw.ord >> shift) & mask
|
|
169
|
+
end
|
|
170
|
+
|
|
149
171
|
def [] x
|
|
150
172
|
raw =
|
|
151
173
|
if @BPP
|
|
@@ -162,6 +184,20 @@ module ZPNG
|
|
|
162
184
|
shift = 8-(x%(8/@bpp)+1)*@bpp
|
|
163
185
|
raise "invalid shift #{shift}" if shift < 0 || shift > 7
|
|
164
186
|
idx = (raw.ord >> shift) & mask
|
|
187
|
+
color = image.palette[idx]
|
|
188
|
+
unless color
|
|
189
|
+
if !@errors.include?(x) && @image.verbose >= -1
|
|
190
|
+
# prevent same error popping up multiple times, f.ex. in zsteg analysis
|
|
191
|
+
@errors << x
|
|
192
|
+
if (32..127).include?(idx)
|
|
193
|
+
msg = '[!] %s: color #%-3d ("%c") at x=%d y=%d is out of palette!'.red % [self.class, idx, idx, x, @idx]
|
|
194
|
+
else
|
|
195
|
+
msg = "[!] %s: color #%-3d at x=%d y=%d is out of palette!".red % [self.class, idx, x, @idx]
|
|
196
|
+
end
|
|
197
|
+
STDERR.puts msg
|
|
198
|
+
end
|
|
199
|
+
color = Color.new(0,0,0)
|
|
200
|
+
end
|
|
165
201
|
if image.trns
|
|
166
202
|
# transparency from tRNS chunk
|
|
167
203
|
# For color type 3 (indexed color), the tRNS chunk contains a series of one-byte alpha values,
|
|
@@ -171,17 +207,14 @@ module ZPNG
|
|
|
171
207
|
# Alpha for palette index 1: 1 byte
|
|
172
208
|
# ...
|
|
173
209
|
#
|
|
174
|
-
color = image.palette[idx].dup
|
|
175
210
|
if color.alpha = image.trns.data[idx]
|
|
176
211
|
# if it's not NULL - convert it from char to int,
|
|
177
212
|
# otherwise it means fully opaque color, as well as NULL alpha in ZPNG::Color
|
|
213
|
+
color = color.dup
|
|
178
214
|
color.alpha = color.alpha.ord
|
|
179
215
|
end
|
|
180
|
-
return color
|
|
181
|
-
else
|
|
182
|
-
# no transparency
|
|
183
|
-
return image.palette[idx]
|
|
184
216
|
end
|
|
217
|
+
return color
|
|
185
218
|
|
|
186
219
|
when COLOR_GRAYSCALE # ALLOWED_DEPTHS: 1, 2, 4, 8, 16
|
|
187
220
|
c = if @bpp == 16
|
data/lib/zpng.rb
CHANGED
|
@@ -16,11 +16,12 @@ require 'zpng/scan_line'
|
|
|
16
16
|
require 'zpng/scan_line/mixins'
|
|
17
17
|
require 'zpng/chunk'
|
|
18
18
|
require 'zpng/text_chunk'
|
|
19
|
-
require 'zpng/readable_struct'
|
|
20
19
|
require 'zpng/adam7_decoder'
|
|
21
20
|
require 'zpng/hexdump'
|
|
22
21
|
require 'zpng/metadata'
|
|
23
22
|
require 'zpng/pixels'
|
|
24
23
|
|
|
25
24
|
require 'zpng/bmp/reader'
|
|
25
|
+
require 'zpng/jpeg/chunks'
|
|
26
|
+
require 'zpng/jpeg/reader'
|
|
26
27
|
require 'zpng/image'
|
data/spec/bad_samples_spec.rb
CHANGED
|
@@ -19,6 +19,18 @@ each_sample("bad/*.png") do |fname|
|
|
|
19
19
|
@img[0,0].should be_instance_of(ZPNG::Color)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
it "accessess all pixels" do
|
|
23
|
+
skip "no BPP" unless @img.bpp
|
|
24
|
+
skip if fname == 'samples/bad/b1.png'
|
|
25
|
+
skip if fname == 'samples/bad/000000.png'
|
|
26
|
+
n = 0
|
|
27
|
+
@img.each_pixel do |px|
|
|
28
|
+
px.should be_instance_of(ZPNG::Color)
|
|
29
|
+
n += 1
|
|
30
|
+
end
|
|
31
|
+
n.should == @img.width*@img.height
|
|
32
|
+
end
|
|
33
|
+
|
|
22
34
|
describe "CLI" do
|
|
23
35
|
it "shows info & chunks" do
|
|
24
36
|
orig_stdout, out = $stdout, ""
|
data/spec/cli_spec.rb
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
require File.expand_path(File.join(File.dirname(__FILE__), '/spec_helper'))
|
|
2
2
|
require 'zpng/cli'
|
|
3
3
|
|
|
4
|
-
CLI_PATHNAME = File.expand_path(File.join(File.dirname(__FILE__), '/../bin/zpng'))
|
|
5
|
-
|
|
6
4
|
describe "CLI" do
|
|
7
5
|
PNGSuite.each_good do |fname|
|
|
8
6
|
describe fname.sub(%r|\A#{Regexp::escape(Dir.getwd)}/?|, '') do
|
data/spec/image_spec.rb
CHANGED
|
@@ -1,51 +1,126 @@
|
|
|
1
|
+
# coding: binary
|
|
1
2
|
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
2
3
|
|
|
3
4
|
NEW_IMG_WIDTH = 20
|
|
4
5
|
NEW_IMG_HEIGHT = 10
|
|
5
6
|
|
|
6
7
|
describe ZPNG::Image do
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
shared_examples "exported image" do |bpp=32|
|
|
10
|
+
let(:eimg){ img.export }
|
|
11
|
+
let(:img2){ ZPNG::Image.new(eimg) }
|
|
12
|
+
|
|
13
|
+
it "has PNG header" do
|
|
14
|
+
eimg.should start_with(ZPNG::Image::PNG_HDR)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe "parsed again" do
|
|
18
|
+
it "is a ZPNG::Image" do
|
|
19
|
+
img2.should be_instance_of(ZPNG::Image)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "should be of specified size" do
|
|
23
|
+
img2.width.should == NEW_IMG_WIDTH
|
|
24
|
+
img2.height.should == NEW_IMG_HEIGHT
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "should have bpp = #{bpp}" do
|
|
28
|
+
img2.hdr.bpp.should == bpp
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "should have 3 chunks: IHDR, IDAT, IEND" do
|
|
32
|
+
img2.chunks.map(&:type).should == %w'IHDR IDAT IEND'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe ".new" do
|
|
39
|
+
let(:img){ ZPNG::Image.new :width => NEW_IMG_WIDTH, :height => NEW_IMG_HEIGHT }
|
|
9
40
|
|
|
10
41
|
it "returns ZPNG::Image" do
|
|
11
42
|
img.should be_instance_of(ZPNG::Image)
|
|
12
43
|
end
|
|
44
|
+
|
|
13
45
|
it "creates new image of specified size" do
|
|
14
46
|
img.width.should == NEW_IMG_WIDTH
|
|
15
47
|
img.height.should == NEW_IMG_HEIGHT
|
|
16
48
|
end
|
|
17
49
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
50
|
+
include_examples "exported image" do
|
|
51
|
+
it "should have all pixels transparent" do
|
|
52
|
+
NEW_IMG_HEIGHT.times do |y|
|
|
53
|
+
NEW_IMG_WIDTH.times do |x|
|
|
54
|
+
img2[x,y].should be_transparent
|
|
55
|
+
end
|
|
56
|
+
end
|
|
22
57
|
end
|
|
58
|
+
end
|
|
23
59
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
60
|
+
describe "setting imagedata" do
|
|
61
|
+
before do
|
|
62
|
+
imagedata_size = NEW_IMG_WIDTH * NEW_IMG_HEIGHT * 4
|
|
63
|
+
imagedata = "\x00" * imagedata_size
|
|
64
|
+
imagedata_size.times do |i|
|
|
65
|
+
imagedata.setbyte(i, i & 0xff)
|
|
29
66
|
end
|
|
67
|
+
img.imagedata = imagedata
|
|
68
|
+
end
|
|
30
69
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
70
|
+
include_examples "exported image" do
|
|
71
|
+
it "should not have all pixels transparent" do
|
|
72
|
+
skip "TBD"
|
|
73
|
+
NEW_IMG_HEIGHT.times do |y|
|
|
74
|
+
NEW_IMG_WIDTH.times do |x|
|
|
75
|
+
img2[x,y].should_not be_transparent
|
|
76
|
+
end
|
|
77
|
+
end
|
|
34
78
|
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
35
81
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe ".from_rgb" do
|
|
85
|
+
before do
|
|
86
|
+
data_size = NEW_IMG_WIDTH * NEW_IMG_HEIGHT * 3
|
|
87
|
+
@data = "\x00" * data_size
|
|
88
|
+
data_size.times do |i|
|
|
89
|
+
@data.setbyte(i, i & 0xff)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
let(:img){ ZPNG::Image.from_rgb(@data, width: NEW_IMG_WIDTH, height: NEW_IMG_HEIGHT) }
|
|
39
94
|
|
|
40
|
-
|
|
41
|
-
|
|
95
|
+
include_examples "exported image", 24 do
|
|
96
|
+
it "should have pixels from passed data" do
|
|
97
|
+
i = (0..255).cycle
|
|
98
|
+
NEW_IMG_HEIGHT.times do |y|
|
|
99
|
+
NEW_IMG_WIDTH.times do |x|
|
|
100
|
+
img2[x,y].should == ZPNG::Color.new(i.next, i.next, i.next)
|
|
101
|
+
end
|
|
42
102
|
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
43
106
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
107
|
+
describe ".from_rgba" do
|
|
108
|
+
before do
|
|
109
|
+
data_size = NEW_IMG_WIDTH * NEW_IMG_HEIGHT * 4
|
|
110
|
+
@data = "\x00" * data_size
|
|
111
|
+
data_size.times do |i|
|
|
112
|
+
@data.setbyte(i, i & 0xff)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
let(:img){ ZPNG::Image.from_rgba(@data, width: NEW_IMG_WIDTH, height: NEW_IMG_HEIGHT) }
|
|
117
|
+
|
|
118
|
+
include_examples "exported image" do
|
|
119
|
+
it "should have pixels from passed data" do
|
|
120
|
+
i = (0..255).cycle
|
|
121
|
+
NEW_IMG_HEIGHT.times do |y|
|
|
122
|
+
NEW_IMG_WIDTH.times do |x|
|
|
123
|
+
img2[x,y].should == ZPNG::Color.new(i.next, i.next, i.next, i.next)
|
|
49
124
|
end
|
|
50
125
|
end
|
|
51
126
|
end
|
data/spec/pixel_access_spec.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require File.expand_path(File.join(File.dirname(__FILE__), '/spec_helper'))
|
|
2
2
|
require 'zpng/cli'
|
|
3
|
+
require 'set'
|
|
3
4
|
|
|
4
5
|
PNGSuite.each_good do |fname|
|
|
5
6
|
describe fname.sub(%r|\A#{Regexp::escape(Dir.getwd)}/?|, '') do
|
|
@@ -12,5 +13,38 @@ PNGSuite.each_good do |fname|
|
|
|
12
13
|
end
|
|
13
14
|
n.should == img.width*img.height
|
|
14
15
|
end
|
|
16
|
+
|
|
17
|
+
it "accessess all pixels with coords" do
|
|
18
|
+
img = ZPNG::Image.load(fname)
|
|
19
|
+
n = 0
|
|
20
|
+
ax = Set.new
|
|
21
|
+
ay = Set.new
|
|
22
|
+
img.each_pixel do |px, x, y|
|
|
23
|
+
px.should be_instance_of(ZPNG::Color)
|
|
24
|
+
n += 1
|
|
25
|
+
ax << x
|
|
26
|
+
ay << y
|
|
27
|
+
end
|
|
28
|
+
n.should == img.width*img.height
|
|
29
|
+
ax.size.should == img.width
|
|
30
|
+
ay.size.should == img.height
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "accessess all pixels using method #2" do
|
|
34
|
+
img = ZPNG::Image.load(fname)
|
|
35
|
+
n = 0
|
|
36
|
+
a = img.each_pixel.to_a
|
|
37
|
+
ax = Set.new
|
|
38
|
+
ay = Set.new
|
|
39
|
+
a.each do |px, x, y|
|
|
40
|
+
px.should be_instance_of(ZPNG::Color)
|
|
41
|
+
n += 1
|
|
42
|
+
ax << x
|
|
43
|
+
ay << y
|
|
44
|
+
end
|
|
45
|
+
n.should == img.width*img.height
|
|
46
|
+
ax.size.should == img.width
|
|
47
|
+
ay.size.should == img.height
|
|
48
|
+
end
|
|
15
49
|
end
|
|
16
50
|
end
|
data/spec/rotate_spec.rb
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
2
|
+
|
|
3
|
+
ROTATE_SAMPLE = File.join(SAMPLES_DIR, "captcha_4bpp.png")
|
|
4
|
+
|
|
5
|
+
include ZPNG
|
|
6
|
+
|
|
7
|
+
describe Image do
|
|
8
|
+
describe "#rotated_90_cw" do
|
|
9
|
+
it "rotates and keeps original image unchanged" do
|
|
10
|
+
src = Image.load(ROTATE_SAMPLE)
|
|
11
|
+
src2 = Image.load(ROTATE_SAMPLE)
|
|
12
|
+
dst = src.rotated_90_cw
|
|
13
|
+
|
|
14
|
+
dst.width.should == src.height
|
|
15
|
+
dst.height.should == src.width
|
|
16
|
+
|
|
17
|
+
dst.width.should_not == src.width
|
|
18
|
+
dst.height.should_not == src.height
|
|
19
|
+
|
|
20
|
+
src.export.should == src2.export
|
|
21
|
+
src.export.should_not == dst.export
|
|
22
|
+
src2.export.should_not == dst.export
|
|
23
|
+
|
|
24
|
+
dst.should == Image.load(File.join(SAMPLES_DIR, "captcha_4bpp_rotated90.png"))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe "#rotated" do
|
|
29
|
+
0.step(360, 90) do |angle|
|
|
30
|
+
it "rotates #{angle} degrees and keeps original image unchanged" do
|
|
31
|
+
src = Image.load(ROTATE_SAMPLE)
|
|
32
|
+
src2 = Image.load(ROTATE_SAMPLE)
|
|
33
|
+
dst = src.rotated(angle)
|
|
34
|
+
dst.save(File.join(SAMPLES_DIR, "captcha_4bpp_rotated#{angle}.png"))
|
|
35
|
+
|
|
36
|
+
if angle % 180 == 0
|
|
37
|
+
dst.width.should == src.width
|
|
38
|
+
dst.height.should == src.height
|
|
39
|
+
else
|
|
40
|
+
dst.width.should == src.height
|
|
41
|
+
dst.height.should == src.width
|
|
42
|
+
|
|
43
|
+
dst.width.should_not == src.width
|
|
44
|
+
dst.height.should_not == src.height
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
src.export.should == src2.export
|
|
48
|
+
|
|
49
|
+
if angle % 360 == 0
|
|
50
|
+
src.export == dst.export
|
|
51
|
+
src2.export == dst.export
|
|
52
|
+
else
|
|
53
|
+
src.export.should_not == dst.export
|
|
54
|
+
src2.export.should_not == dst.export
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
src = Image.load(angle % 360 == 0 ? ROTATE_SAMPLE : File.join(SAMPLES_DIR, "captcha_4bpp_rotated#{angle}.png"))
|
|
58
|
+
dst.should == src
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/zpng.gemspec
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
|
3
3
|
# Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
|
|
4
4
|
# -*- encoding: utf-8 -*-
|
|
5
|
-
# stub: zpng 0.4.
|
|
5
|
+
# stub: zpng 0.4.6 ruby lib
|
|
6
6
|
|
|
7
7
|
Gem::Specification.new do |s|
|
|
8
8
|
s.name = "zpng".freeze
|
|
9
|
-
s.version = "0.4.
|
|
9
|
+
s.version = "0.4.6"
|
|
10
10
|
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
|
12
12
|
s.require_paths = ["lib".freeze]
|
|
13
13
|
s.authors = ["Andrey \"Zed\" Zaikin".freeze]
|
|
14
|
-
s.date = "
|
|
14
|
+
s.date = "2026-01-28"
|
|
15
15
|
s.email = "zed.0xff@gmail.com".freeze
|
|
16
16
|
s.executables = ["zpng".freeze]
|
|
17
17
|
s.extra_rdoc_files = [
|
|
@@ -42,9 +42,10 @@ Gem::Specification.new do |s|
|
|
|
42
42
|
"lib/zpng/deep_copyable.rb",
|
|
43
43
|
"lib/zpng/hexdump.rb",
|
|
44
44
|
"lib/zpng/image.rb",
|
|
45
|
+
"lib/zpng/jpeg/chunks.rb",
|
|
46
|
+
"lib/zpng/jpeg/reader.rb",
|
|
45
47
|
"lib/zpng/metadata.rb",
|
|
46
48
|
"lib/zpng/pixels.rb",
|
|
47
|
-
"lib/zpng/readable_struct.rb",
|
|
48
49
|
"lib/zpng/scan_line.rb",
|
|
49
50
|
"lib/zpng/scan_line/mixins.rb",
|
|
50
51
|
"lib/zpng/string_ext.rb",
|
|
@@ -68,6 +69,7 @@ Gem::Specification.new do |s|
|
|
|
68
69
|
"spec/modify_spec.rb",
|
|
69
70
|
"spec/pixel_access_spec.rb",
|
|
70
71
|
"spec/pixels_enumerator_spec.rb",
|
|
72
|
+
"spec/rotate_spec.rb",
|
|
71
73
|
"spec/running_pixel_spec.rb",
|
|
72
74
|
"spec/set_random_pixel_spec.rb",
|
|
73
75
|
"spec/spec_helper.rb",
|
|
@@ -76,7 +78,7 @@ Gem::Specification.new do |s|
|
|
|
76
78
|
]
|
|
77
79
|
s.homepage = "http://github.com/zed-0xff/zpng".freeze
|
|
78
80
|
s.licenses = ["MIT".freeze]
|
|
79
|
-
s.rubygems_version = "3.
|
|
81
|
+
s.rubygems_version = "3.2.33".freeze
|
|
80
82
|
s.summary = "pure ruby PNG file manipulation & validation".freeze
|
|
81
83
|
|
|
82
84
|
if s.respond_to? :specification_version then
|
|
@@ -85,11 +87,13 @@ Gem::Specification.new do |s|
|
|
|
85
87
|
|
|
86
88
|
if s.respond_to? :add_runtime_dependency then
|
|
87
89
|
s.add_runtime_dependency(%q<rainbow>.freeze, ["~> 3.1.1"])
|
|
90
|
+
s.add_runtime_dependency(%q<iostruct>.freeze, [">= 0.7.0"])
|
|
88
91
|
s.add_development_dependency(%q<rspec>.freeze, ["~> 3.11.0"])
|
|
89
92
|
s.add_development_dependency(%q<rspec-its>.freeze, ["~> 1.3.0"])
|
|
90
93
|
s.add_development_dependency(%q<juwelier>.freeze, ["~> 2.4.9"])
|
|
91
94
|
else
|
|
92
95
|
s.add_dependency(%q<rainbow>.freeze, ["~> 3.1.1"])
|
|
96
|
+
s.add_dependency(%q<iostruct>.freeze, [">= 0.7.0"])
|
|
93
97
|
s.add_dependency(%q<rspec>.freeze, ["~> 3.11.0"])
|
|
94
98
|
s.add_dependency(%q<rspec-its>.freeze, ["~> 1.3.0"])
|
|
95
99
|
s.add_dependency(%q<juwelier>.freeze, ["~> 2.4.9"])
|