zpng 0.4.5 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 99e5f2ae6f86a27757f442011a5fa365b06f4a4bf52c1348a6018bb0cc8b5f85
4
- data.tar.gz: f435672e701c25224d5e61dba082131608567fbe14d86fbd4065495d460d2932
3
+ metadata.gz: 45cbe6009082c09bd91263c35aafcde94fe9a244f30999a8181a42cde9d8c591
4
+ data.tar.gz: bfb0767ed14b07544ca7159454b9dbe5ccdd404f98f7e293dddb52b13abf43aa
5
5
  SHA512:
6
- metadata.gz: ce27f95e40b522d1dd854ecb4619650e22a6fc2b5bd4c9aee6c3247e5d32bf98da6778343645723359bb05872e33a68cad9ad08dbc2fae7c0f09c123d6567675
7
- data.tar.gz: 3f9ea93a539c4ef1f16900eab6b9e4a8e8f2be7aceed695778cf296dd151df642e1da8b4bff3ff23f34aa560c660816d99d46d462d431547f32455fcfa7375e8
6
+ metadata.gz: f3929af614a96f549e331a65c15f210bc96bd52c5d88625d85156eb507773b89fc010d968ceddd25de213124db0dc935821dc02f255555eff4780ff20b87eb91
7
+ data.tar.gz: 1d5ab750a6f0aa24fe710ee53b53240d0b3a4335e3484c6c24eb99419413acc3701af9bd7aa5c3928b6971344e0759ee1843e5dbeed09004d06906ef5f9d0a4c
data/Gemfile CHANGED
@@ -3,6 +3,7 @@ source "http://rubygems.org"
3
3
  # Example:
4
4
  # gem "activesupport", ">= 2.3.5"
5
5
  gem 'rainbow', '~> 3.1.1'
6
+ gem "iostruct", ">= 0.7.0"
6
7
 
7
8
  # Add dependencies to develop your gem here.
8
9
  # Include everything needed to run rake, tests, features, etc.
data/Gemfile.lock CHANGED
@@ -1,13 +1,36 @@
1
1
  GEM
2
2
  remote: http://rubygems.org/
3
3
  specs:
4
- addressable (2.8.0)
5
- public_suffix (>= 2.0.2, < 5.0)
6
- builder (3.2.4)
4
+ activesupport (7.1.6)
5
+ base64
6
+ benchmark (>= 0.3)
7
+ bigdecimal
8
+ concurrent-ruby (~> 1.0, >= 1.0.2)
9
+ connection_pool (>= 2.2.5)
10
+ drb
11
+ i18n (>= 1.6, < 2)
12
+ logger (>= 1.4.2)
13
+ minitest (>= 5.1)
14
+ mutex_m
15
+ securerandom (>= 0.3)
16
+ tzinfo (~> 2.0)
17
+ addressable (2.8.8)
18
+ public_suffix (>= 2.0.2, < 8.0)
19
+ base64 (0.3.0)
20
+ benchmark (0.5.0)
21
+ bigdecimal (4.0.1)
22
+ builder (3.3.0)
23
+ cgi (0.5.1)
24
+ concurrent-ruby (1.3.6)
25
+ connection_pool (2.5.5)
26
+ date (3.5.1)
7
27
  descendants_tracker (0.0.4)
8
28
  thread_safe (~> 0.3, >= 0.3.1)
9
- diff-lcs (1.5.0)
10
- faraday (1.10.0)
29
+ diff-lcs (1.6.2)
30
+ drb (2.2.3)
31
+ erb (4.0.4)
32
+ cgi (>= 0.3.3)
33
+ faraday (1.10.4)
11
34
  faraday-em_http (~> 1.0)
12
35
  faraday-em_synchrony (~> 1.0)
13
36
  faraday-excon (~> 1.1)
@@ -20,18 +43,20 @@ GEM
20
43
  faraday-retry (~> 1.0)
21
44
  ruby2_keywords (>= 0.0.4)
22
45
  faraday-em_http (1.0.0)
23
- faraday-em_synchrony (1.0.0)
46
+ faraday-em_synchrony (1.0.1)
24
47
  faraday-excon (1.1.0)
25
48
  faraday-httpclient (1.0.1)
26
- faraday-multipart (1.0.4)
27
- multipart-post (~> 2)
28
- faraday-net_http (1.0.1)
49
+ faraday-multipart (1.2.0)
50
+ multipart-post (~> 2.0)
51
+ faraday-net_http (1.0.2)
29
52
  faraday-net_http_persistent (1.2.0)
30
53
  faraday-patron (1.0.0)
31
54
  faraday-rack (1.0.0)
32
55
  faraday-retry (1.0.3)
33
- git (1.13.1)
56
+ git (2.3.3)
57
+ activesupport (>= 5.0)
34
58
  addressable (~> 2.8)
59
+ process_executer (~> 1.1)
35
60
  rchardet (~> 1.8)
36
61
  github_api (0.19.0)
37
62
  addressable (~> 2.4)
@@ -40,7 +65,12 @@ GEM
40
65
  hashie (~> 3.5, >= 3.5.2)
41
66
  oauth2 (~> 1.0)
42
67
  hashie (3.6.0)
43
- highline (2.0.3)
68
+ highline (3.1.2)
69
+ reline
70
+ i18n (1.14.8)
71
+ concurrent-ruby (~> 1.0)
72
+ io-console (0.8.2)
73
+ iostruct (0.7.0)
44
74
  juwelier (2.4.9)
45
75
  builder
46
76
  bundler
@@ -53,15 +83,19 @@ GEM
53
83
  rake
54
84
  rdoc
55
85
  semver2
56
- jwt (2.4.1)
86
+ jwt (2.10.2)
87
+ base64
57
88
  kamelcase (0.0.2)
58
89
  semver2 (~> 3)
59
- mini_portile2 (2.8.1)
60
- multi_json (1.15.0)
90
+ logger (1.7.0)
91
+ mini_portile2 (2.8.9)
92
+ minitest (5.26.1)
93
+ multi_json (1.19.1)
61
94
  multi_xml (0.6.0)
62
- multipart-post (2.2.3)
63
- nokogiri (1.14.2)
64
- mini_portile2 (~> 2.8.0)
95
+ multipart-post (2.4.1)
96
+ mutex_m (0.3.0)
97
+ nokogiri (1.17.2)
98
+ mini_portile2 (~> 2.8.2)
65
99
  racc (~> 1.4)
66
100
  oauth2 (1.4.11)
67
101
  faraday (>= 0.17.3, < 3.0)
@@ -69,45 +103,56 @@ GEM
69
103
  multi_json (~> 1.3)
70
104
  multi_xml (~> 0.5)
71
105
  rack (>= 1.2, < 4)
72
- psych (4.0.4)
106
+ process_executer (1.1.2)
107
+ psych (5.3.1)
108
+ date
73
109
  stringio
74
- public_suffix (4.0.7)
75
- racc (1.6.2)
76
- rack (3.0.4.1)
110
+ public_suffix (6.0.2)
111
+ racc (1.8.1)
112
+ rack (3.2.4)
77
113
  rainbow (3.1.1)
78
- rake (13.0.6)
79
- rchardet (1.8.0)
80
- rdoc (6.4.0)
114
+ rake (13.3.1)
115
+ rchardet (1.10.0)
116
+ rdoc (7.1.0)
117
+ erb
81
118
  psych (>= 4.0.0)
119
+ tsort
120
+ reline (0.6.3)
121
+ io-console (~> 0.5)
82
122
  rspec (3.11.0)
83
123
  rspec-core (~> 3.11.0)
84
124
  rspec-expectations (~> 3.11.0)
85
125
  rspec-mocks (~> 3.11.0)
86
126
  rspec-core (3.11.0)
87
127
  rspec-support (~> 3.11.0)
88
- rspec-expectations (3.11.0)
128
+ rspec-expectations (3.11.1)
89
129
  diff-lcs (>= 1.2.0, < 2.0)
90
130
  rspec-support (~> 3.11.0)
91
- rspec-its (1.3.0)
131
+ rspec-its (1.3.1)
92
132
  rspec-core (>= 3.0.0)
93
133
  rspec-expectations (>= 3.0.0)
94
- rspec-mocks (3.11.1)
134
+ rspec-mocks (3.11.2)
95
135
  diff-lcs (>= 1.2.0, < 2.0)
96
136
  rspec-support (~> 3.11.0)
97
- rspec-support (3.11.0)
137
+ rspec-support (3.11.1)
98
138
  ruby2_keywords (0.0.5)
139
+ securerandom (0.3.2)
99
140
  semver2 (3.4.2)
100
- stringio (3.0.2)
141
+ stringio (3.2.0)
101
142
  thread_safe (0.3.6)
143
+ tsort (0.2.0)
144
+ tzinfo (2.0.6)
145
+ concurrent-ruby (~> 1.0)
102
146
 
103
147
  PLATFORMS
104
148
  ruby
105
149
 
106
150
  DEPENDENCIES
151
+ iostruct (>= 0.7.0)
107
152
  juwelier (~> 2.4.9)
108
153
  rainbow (~> 3.1.1)
109
154
  rspec (~> 3.11.0)
110
155
  rspec-its (~> 1.3.0)
111
156
 
112
157
  BUNDLED WITH
113
- 2.3.12
158
+ 2.4.22
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.5
1
+ 0.4.6
@@ -1,22 +1,27 @@
1
+ # -*- coding:binary; frozen_string_literal: true -*-
2
+ require 'iostruct'
3
+
1
4
  module ZPNG
2
5
  module BMP
3
6
 
4
- class BITMAPFILEHEADER < ReadableStruct.new 'VvvV', #a2VvvV',
5
- #:bfType,
6
- :bfSize, # the size of the BMP file in bytes
7
+ MAGIC = "BM"
8
+
9
+ class BITMAPFILEHEADER < IOStruct.new 'VvvV',
10
+ #:bfType, # read separately as magic bytes
11
+ :bfSize, # the size of the BMP file in bytes
7
12
  :bfReserved1,
8
13
  :bfReserved2,
9
- :bfOffBits # imagedata offset
14
+ :bfOffBits # imagedata offset
10
15
 
11
16
  def inspect
12
17
  "<" + super.partition(self.class.to_s.split('::').last)[1..-1].join
13
18
  end
14
19
  end
15
20
 
16
- class BITMAPINFOHEADER < ReadableStruct.new 'V3v2V6',
21
+ class BITMAPINFOHEADER < IOStruct.new 'Vl2v2V2l2V2', # l2 for signed width/height and pels/meter
17
22
  :biSize, # BITMAPINFOHEADER::SIZE
18
- :biWidth,
19
- :biHeight,
23
+ :biWidth, # can be negative for top-down DIB
24
+ :biHeight, # can be negative for top-down DIB
20
25
  :biPlanes,
21
26
  :biBitCount,
22
27
  :biCompression,
data/lib/zpng/cli.rb CHANGED
@@ -177,11 +177,11 @@ module ZPNG
177
177
 
178
178
  def info
179
179
  color = %w'COLOR_GRAYSCALE COLOR_RGB COLOR_INDEXED COLOR_GRAY_ALPHA COLOR_RGBA'.find do |k|
180
- @img.hdr.color == ZPNG.const_get(k)
180
+ @img.hdr&.color == ZPNG.const_get(k)
181
181
  end
182
182
  puts "[.] image size #{@img.width || '?'}x#{@img.height || '?'}, #{@img.bpp || '?'}bpp, #{color}"
183
183
  puts "[.] palette = #{@img.palette}" if @img.palette
184
- puts "[.] uncompressed imagedata size = #{@img.imagedata_size || '?'} bytes"
184
+ puts "[.] uncompressed imagedata size = #{@img.imagedata_size} bytes" if @img.imagedata_size.to_i > 0
185
185
  _conditional_hexdump(@img.imagedata, 3) if @options[:verbose] > 0
186
186
  end
187
187
 
data/lib/zpng/color.rb CHANGED
@@ -6,6 +6,8 @@ module ZPNG
6
6
 
7
7
  include DeepCopyable
8
8
 
9
+ MAX_VALUES = 17.times.map{ |x| (2**x)-1 }.freeze
10
+
9
11
  def initialize *a
10
12
  h = a.last.is_a?(Hash) ? a.pop : {}
11
13
  @r,@g,@b,@a = *a
@@ -14,11 +16,15 @@ module ZPNG
14
16
  @depth = h[:depth] || 8
15
17
 
16
18
  # default ALPHA = 0xff - opaque
17
- @a ||= h[:alpha] || h[:a] || (2**@depth-1)
19
+ @a ||= h[:alpha] || h[:a] || max_value
20
+ end
21
+
22
+ def max_value
23
+ MAX_VALUES[@depth]
18
24
  end
19
25
 
20
26
  def a= a
21
- @a = a || (2**@depth-1) # NULL alpha means fully opaque
27
+ @a = a || max_value # NULL alpha means fully opaque
22
28
  end
23
29
  alias :alpha :a
24
30
  alias :alpha= :a=
@@ -64,8 +70,7 @@ module ZPNG
64
70
  end
65
71
 
66
72
  def white?
67
- max = 2**depth-1
68
- r == max && g == max && b == max
73
+ r == max_value && g == max_value && b == max_value
69
74
  end
70
75
 
71
76
  def black?
@@ -77,7 +82,7 @@ module ZPNG
77
82
  end
78
83
 
79
84
  def opaque?
80
- a.nil? || a == 2**depth-1
85
+ a.nil? || a == max_value
81
86
  end
82
87
 
83
88
  def to_grayscale
@@ -135,7 +140,7 @@ module ZPNG
135
140
  # try to convert to one pseudographics ASCII character
136
141
  def to_ascii map=ASCII_MAP
137
142
  #p self
138
- map[self.to_grayscale*(map.size-1)/(2**@depth-1), 1]
143
+ map[self.to_grayscale*(map.size-1)/max_value, 1]
139
144
  end
140
145
 
141
146
  # convert to ANSI color name
@@ -162,7 +167,7 @@ module ZPNG
162
167
  color = Color.new :depth => new_depth
163
168
  if new_depth > self.depth
164
169
  %w'r g b a'.each do |part|
165
- color.send("#{part}=", (2**new_depth-1)/(2**depth-1)*self.send(part))
170
+ color.send("#{part}=", (2**new_depth-1)/max_value*self.send(part))
166
171
  end
167
172
  else
168
173
  # new_depth < self.depth
@@ -242,27 +247,96 @@ module ZPNG
242
247
  end
243
248
 
244
249
  # Op! op! op! Op!! Oppan Gangnam Style!!
245
- def op op, c=nil
246
- # XXX what to do with alpha?
247
- max = 2**depth-1
250
+ def op op, c=nil, op2=:&
251
+ # alpha is kept from 1st color
248
252
  if c
249
253
  c = c.to_depth(depth)
250
254
  Color.new(
251
- @r.send(op, c.r) & max,
252
- @g.send(op, c.g) & max,
253
- @b.send(op, c.b) & max,
254
- :depth => self.depth
255
+ @r.send(op, c.r).send(op2, max_value),
256
+ @g.send(op, c.g).send(op2, max_value),
257
+ @b.send(op, c.b).send(op2, max_value),
258
+ # [0, [@r.send(op, c.r), max_value].min].max,
259
+ # [0, [@g.send(op, c.g), max_value].min].max,
260
+ # [0, [@b.send(op, c.b), max_value].min].max,
261
+ depth: depth,
262
+ alpha: alpha
255
263
  )
256
264
  else
257
265
  Color.new(
258
- @r.send(op) & max,
259
- @g.send(op) & max,
260
- @b.send(op) & max,
261
- :depth => self.depth
266
+ @r.send(op).send(op2, max_value),
267
+ @g.send(op).send(op2, max_value),
268
+ @b.send(op).send(op2, max_value),
269
+ # [0, [@r.send(op), max_value].min].max,
270
+ # [0, [@g.send(op), max_value].min].max,
271
+ # [0, [@b.send(op), max_value].min].max,
272
+ depth: depth,
273
+ alpha: alpha
262
274
  )
263
275
  end
264
276
  end
265
277
 
278
+ # multiplies the pixel values of the upper layer with those of the layer below it and then divides the result by MAX_VALUE
279
+ def * c
280
+ c = c.to_depth(depth)
281
+ Color.new(
282
+ (@r * c.r) / max_value,
283
+ (@g * c.g) / max_value,
284
+ (@b * c.b) / max_value,
285
+ depth: depth,
286
+ alpha: alpha
287
+ )
288
+ end
289
+
290
+ def / c
291
+ c = c.to_depth(depth)
292
+ Color.new(
293
+ [max_value, (max_value*@r/c.r)].min,
294
+ [max_value, (max_value*@g/c.g)].min,
295
+ [max_value, (max_value*@b/c.b)].min,
296
+ # (max_value*@r/c.r),
297
+ # (max_value*@g/c.g),
298
+ # (max_value*@b/c.b),
299
+ depth: depth,
300
+ alpha: alpha
301
+ )
302
+ rescue ZeroDivisionError
303
+ c = c.dup
304
+ c.r = 1 if c.r == 0 # XXX or it should be max_value ?
305
+ c.g = 1 if c.g == 0
306
+ c.b = 1 if c.b == 0
307
+ return Color.new(
308
+ [max_value, (max_value*@r/c.r)].min,
309
+ [max_value, (max_value*@g/c.g)].min,
310
+ [max_value, (max_value*@b/c.b)].min,
311
+ depth: depth,
312
+ alpha: alpha
313
+ )
314
+ end
315
+
316
+ def divmul c1, c2
317
+ c1 = c1.to_depth(depth)
318
+ c2 = c2.to_depth(depth)
319
+ Color.new(
320
+ [max_value, (c2.r*@r/c1.r)].min,
321
+ [max_value, (c2.g*@g/c1.g)].min,
322
+ [max_value, (c2.b*@b/c1.b)].min,
323
+ depth: depth,
324
+ alpha: alpha
325
+ )
326
+ end
327
+
328
+ # http://www.pegtop.net/delphi/articles/blendmodes/screen.htm
329
+ def screen c
330
+ c = c.to_depth(depth)
331
+ Color.new(
332
+ max_value - (((max_value-@r) * (max_value-c.r)) >> depth),
333
+ max_value - (((max_value-@g) * (max_value-c.g)) >> depth),
334
+ max_value - (((max_value-@b) * (max_value-c.b)) >> depth),
335
+ depth: depth,
336
+ alpha: alpha
337
+ )
338
+ end
339
+
266
340
  # for Array.uniq()
267
341
  def hash
268
342
  self.to_i
data/lib/zpng/image.rb CHANGED
@@ -1,8 +1,9 @@
1
+ # -*- coding:binary; frozen_string_literal: true -*-
1
2
  require 'stringio'
2
3
 
3
4
  module ZPNG
4
5
  class Image
5
- attr_accessor :chunks, :scanlines, :imagedata, :extradata, :format, :verbose
6
+ attr_accessor :chunks, :scanlines, :extradata, :format, :verbose
6
7
 
7
8
  # now only for (limited) BMP support
8
9
  attr_accessor :color_class
@@ -12,15 +13,21 @@ module ZPNG
12
13
  alias :dup :deep_copy
13
14
 
14
15
  include BMP::Reader
16
+ include JPEG::Reader
15
17
 
16
- PNG_HDR = "\x89PNG\x0d\x0a\x1a\x0a".force_encoding('binary')
17
- BMP_HDR = "BM".force_encoding('binary')
18
+ PNG_HDR = "\x89PNG\x0d\x0a\x1a\x0a"
18
19
 
19
20
  # possible input params:
20
21
  # IO of opened image file
21
22
  # String with image file already readed
22
23
  # Hash of image parameters to create new blank image
24
+ # width, height
23
25
  def initialize x, h={}
26
+ if x.is_a?(Numeric) && h.is_a?(Numeric)
27
+ x = { width: x, height: h }
28
+ h = {}
29
+ end
30
+
24
31
  @chunks = []
25
32
  @extradata = []
26
33
  @color_class = Color
@@ -74,6 +81,18 @@ module ZPNG
74
81
  end
75
82
  alias :load_file :load
76
83
  alias :from_file :load # as in ChunkyPNG
84
+
85
+ def from_rgb data, width:, height:
86
+ img = new(width: width, height: height, bpp: 24)
87
+ img.scanlines = height.times.map{ |i| ScanLine.new(img, i, decoded_bytes: data[width*3*i, width*3]) }
88
+ img
89
+ end
90
+
91
+ def from_rgba data, width:, height:
92
+ img = new(width: width, height: height, bpp: 32)
93
+ img.scanlines = height.times.map{ |i| ScanLine.new(img, i, decoded_bytes: data[width*4*i, width*4]) }
94
+ img
95
+ end
77
96
  end
78
97
 
79
98
  # save image to file
@@ -154,11 +173,13 @@ module ZPNG
154
173
  # Once a stream is in binary mode, it cannot be reset to nonbinary mode.
155
174
  io.binmode
156
175
 
157
- hdr = io.read(BMP_HDR.size)
158
- if hdr == BMP_HDR
176
+ hdr = io.read(BMP::MAGIC.size)
177
+ if hdr == BMP::MAGIC
159
178
  _read_bmp io
179
+ elsif hdr == JPEG::MAGIC
180
+ _read_jpeg io
160
181
  else
161
- hdr << io.read(PNG_HDR.size - BMP_HDR.size)
182
+ hdr << io.read(PNG_HDR.size - BMP::MAGIC.size)
162
183
  if hdr == PNG_HDR
163
184
  _read_png io
164
185
  else
@@ -276,7 +297,7 @@ module ZPNG
276
297
  # on errors keep going and try to return maximum possible data
277
298
  def _safe_inflate data
278
299
  zi = Zlib::Inflate.new
279
- pos = 0; r = ''
300
+ pos = 0; r = String.new
280
301
  begin
281
302
  # save some memory by not using String#[] when not necessary
282
303
  r << zi.inflate(pos==0 ? data : data[pos..-1])
@@ -326,6 +347,11 @@ module ZPNG
326
347
  end
327
348
  end
328
349
 
350
+ def imagedata= data
351
+ @scanlines = nil
352
+ @imagedata = data
353
+ end
354
+
329
355
  def imagedata_size
330
356
  if new_image?
331
357
  @scanlines.map(&:size).inject(&:+)
@@ -409,6 +435,18 @@ module ZPNG
409
435
  end
410
436
  end
411
437
 
438
+ def to_ansi wide: false
439
+ spc = wide ? " " : " "
440
+ r = String.new
441
+ height.times do |y|
442
+ width.times do |x|
443
+ r << spc.background(self[x,y].to_ansi)
444
+ end
445
+ r << "\n"
446
+ end
447
+ r
448
+ end
449
+
412
450
  def extract_block x,y=nil,w=nil,h=nil
413
451
  if x.is_a?(Hash)
414
452
  Block.new(self,x[:x], x[:y], x[:width], x[:height])
@@ -487,11 +525,10 @@ module ZPNG
487
525
  end
488
526
 
489
527
  # returns new image
490
- def crop params
491
- decode_all_scanlines
492
- # deep copy first, then crop!
493
- deep_copy.crop!(params)
528
+ def cropped **args
529
+ dup.crop!(**args)
494
530
  end
531
+ alias crop cropped
495
532
 
496
533
  def pixels
497
534
  Pixels.new(self)
@@ -547,5 +584,133 @@ module ZPNG
547
584
 
548
585
  new_img
549
586
  end
587
+
588
+ def _normalize_rotate x
589
+ x = x.to_i
590
+ x %= 360
591
+ x += 360 if x < 0
592
+ raise "invalid rotate: #{x}" if x%90 != 0
593
+ x
594
+ end
595
+
596
+ # always returns a copy
597
+ def rotated degrees
598
+ degrees = _normalize_rotate(degrees)
599
+ return dup if degrees == 0
600
+
601
+ dst = self
602
+ while degrees > 0
603
+ dst = dst.rotated_90_cw
604
+ degrees -= 90
605
+ end
606
+ dst
607
+ end
608
+
609
+ # returns new image rotated 90 degrees clockwise
610
+ def rotated_90_cw
611
+ dst = Image.new(width: height, height: width, bpp: bpp)
612
+ each_pixel do |c,x,y|
613
+ dst[height-y-1,x] = c
614
+ end
615
+ dst
616
+ end
617
+
618
+ def copy_from(src, copy_transparent: false,
619
+ src_x: 0, src_y: 0, src_width: src.width, src_height: src.height,
620
+ dst_x: 0, dst_y: 0, dst_width: src_width, dst_height: src_height)
621
+
622
+ dst_height.times do |iy|
623
+ dy = dst_y + iy
624
+ next if dy >= height
625
+ sy = src_y + iy * src_height / dst_height
626
+ next if sy >= src.height
627
+ dst_width.times do |ix|
628
+ dx = dst_x + ix
629
+ next if dx >= width
630
+ sx = src_x + ix * src_width / dst_width
631
+ next if sx >= src.width
632
+ c = src[sx,sy]
633
+ next if c.transparent? && !copy_transparent
634
+ self[dx,dy] = c
635
+ end
636
+ end
637
+ self
638
+ end
639
+
640
+ # op is a symbol of operation, like :+, :-, :* ...
641
+ def op_from(src, op,
642
+ src_x: 0, src_y: 0, src_width: src.width, src_height: src.height,
643
+ dst_x: 0, dst_y: 0, dst_width: src_width, dst_height: src_height)
644
+
645
+ dst_height.times do |iy|
646
+ dy = dst_y + iy
647
+ next if dy >= height
648
+ sy = src_y + iy * src_height / dst_height
649
+ next if sy >= src.height
650
+ dst_width.times do |ix|
651
+ dx = dst_x + ix
652
+ next if dx >= width
653
+ sx = src_x + ix * src_width / dst_width
654
+ next if sx >= src.width
655
+ c = src[sx,sy]
656
+ self[dx,dy] = self[dx,dy].send(op, c)
657
+ end
658
+ end
659
+ self
660
+ end
661
+
662
+ def scale(x, y=x)
663
+ dst = Image.new(width: width*x, height: height*y, bpp: bpp)
664
+ dst.copy_from(self, dst_width: dst.width, dst_height: dst.height)
665
+ end
666
+
667
+ alias scaled scale
668
+
669
+ def shear(mx, my)
670
+ src = self
671
+ dst = Image.new(width: width+(mx*height).abs, height: height+(my*width).abs, bpp: bpp)
672
+ xadd = mx < 0 ? src.height: 0
673
+ yadd = my < 0 ? src.width : 0
674
+ each_pixel do |c,x,y|
675
+ dst[xadd + (x+mx*y).to_i, yadd + (y+my*x).to_i] = c
676
+ end
677
+ dst
678
+ end
679
+
680
+ def empty?
681
+ pixels.all?(&:transparent?)
682
+ end
683
+
684
+ def * value; op(:*, value); end
685
+ def / value; op(:/, value); end
686
+ def + value; op(:+, value); end
687
+ def - value; op(:-, value); end
688
+
689
+ def op op, value
690
+ case value
691
+ when Image
692
+ dst = Image.new(width: width, height: height, bpp: bpp)
693
+ each_pixel do |c,x,y|
694
+ dst[x,y] = c.send(op, value[x,y])
695
+ end
696
+ dst
697
+ when Color
698
+ dst = Image.new(width: width, height: height, bpp: bpp)
699
+ each_pixel do |c,x,y|
700
+ dst[x,y] = c.send(op, value)
701
+ end
702
+ dst
703
+ else
704
+ raise ArgumentError, "cannot #{op} Image by #{value}"
705
+ end
706
+ end
707
+
708
+ def divmul c1, c2
709
+ dst = Image.new(width: width, height: height, bpp: bpp)
710
+ each_pixel do |c,x,y|
711
+ dst[x,y] = c.divmul(c1, c2)
712
+ end
713
+ dst
714
+ end
550
715
  end
551
716
  end
@@ -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.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/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
@@ -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 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.5"
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-19"
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"])
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zpng
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
4
+ version: 0.4.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrey "Zed" Zaikin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-02-19 00:00:00.000000000 Z
11
+ date: 2026-01-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rainbow
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: 3.1.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: iostruct
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.7.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 0.7.0
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rspec
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -98,9 +112,10 @@ files:
98
112
  - lib/zpng/deep_copyable.rb
99
113
  - lib/zpng/hexdump.rb
100
114
  - lib/zpng/image.rb
115
+ - lib/zpng/jpeg/chunks.rb
116
+ - lib/zpng/jpeg/reader.rb
101
117
  - lib/zpng/metadata.rb
102
118
  - lib/zpng/pixels.rb
103
- - lib/zpng/readable_struct.rb
104
119
  - lib/zpng/scan_line.rb
105
120
  - lib/zpng/scan_line/mixins.rb
106
121
  - lib/zpng/string_ext.rb
@@ -124,6 +139,7 @@ files:
124
139
  - spec/modify_spec.rb
125
140
  - spec/pixel_access_spec.rb
126
141
  - spec/pixels_enumerator_spec.rb
142
+ - spec/rotate_spec.rb
127
143
  - spec/running_pixel_spec.rb
128
144
  - spec/set_random_pixel_spec.rb
129
145
  - spec/spec_helper.rb
@@ -148,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
148
164
  - !ruby/object:Gem::Version
149
165
  version: '0'
150
166
  requirements: []
151
- rubygems_version: 3.3.7
167
+ rubygems_version: 3.2.33
152
168
  signing_key:
153
169
  specification_version: 4
154
170
  summary: pure ruby PNG file manipulation & validation
@@ -1,56 +0,0 @@
1
- module ZPNG
2
- module ReadableStruct
3
-
4
- def self.new fmt, *args
5
- size = fmt.scan(/([a-z])(\d*)/i).map do |f,len|
6
- [len.to_i, 1].max *
7
- case f
8
- when /[aAC]/ then 1
9
- when 'v' then 2
10
- when 'V','l' then 4
11
- when 'Q' then 8
12
- else raise "unknown fmt #{f.inspect}"
13
- end
14
- end.inject(&:+)
15
-
16
- Struct.new( *args ).tap do |x|
17
- x.const_set 'FORMAT', fmt
18
- x.const_set 'SIZE', size
19
- x.class_eval do
20
- include InstanceMethods
21
- end
22
- x.extend ClassMethods
23
- end
24
- end
25
-
26
- module ClassMethods
27
- # src can be IO or String, or anything that responds to :read or :unpack
28
- def read src, size = nil
29
- size ||= const_get 'SIZE'
30
- data =
31
- if src.respond_to?(:read)
32
- src.read(size).to_s
33
- elsif src.respond_to?(:unpack)
34
- src
35
- else
36
- raise "[?] don't know how to read from #{src.inspect}"
37
- end
38
- if data.size < size
39
- $stderr.puts "[!] #{self.to_s} want #{size} bytes, got #{data.size}"
40
- end
41
- new(*data.unpack(const_get('FORMAT')))
42
- end
43
- end
44
-
45
- module InstanceMethods
46
- def pack
47
- to_a.pack self.class.const_get('FORMAT')
48
- end
49
-
50
- def empty?
51
- to_a.all?{ |t| t == 0 || t.nil? || t.to_s.tr("\x00","").empty? }
52
- end
53
- end
54
-
55
- end # ReadableStruct
56
- end # ZPNG