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.
@@ -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
@@ -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'
@@ -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
- describe "new" do
8
- let!(:img){ ZPNG::Image.new :width => NEW_IMG_WIDTH, :height => NEW_IMG_HEIGHT }
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
- describe "exported image" do
19
- let!(:eimg){ img.export }
20
- it "has PNG header" do
21
- eimg.should start_with(ZPNG::Image::PNG_HDR)
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
- describe "parsed again" do
25
- let!(:img2){ ZPNG::Image.new(eimg) }
26
-
27
- it "is a ZPNG::Image" do
28
- img2.should be_instance_of(ZPNG::Image)
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
- it "should be of specified size" do
32
- img2.width.should == NEW_IMG_WIDTH
33
- img2.height.should == NEW_IMG_HEIGHT
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
- it "should have bpp = 32" do
37
- img2.hdr.bpp.should == 32
38
- end
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
- it "should have 3 chunks: IHDR, IDAT, IEND" do
41
- img2.chunks.map(&:type).should == %w'IHDR IDAT IEND'
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
- it "should have all pixels transparent" do
45
- NEW_IMG_HEIGHT.times do |y|
46
- NEW_IMG_WIDTH.times do |x|
47
- img2[x,y].should be_transparent
48
- end
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
@@ -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
@@ -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.4 ruby lib
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.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 = "2023-02-14"
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.3.7".freeze
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"])