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 +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 +176 -11
- data/lib/zpng/jpeg/chunks.rb +164 -0
- data/lib/zpng/jpeg/reader.rb +55 -0
- data/lib/zpng.rb +2 -1
- data/spec/cli_spec.rb +0 -2
- data/spec/image_spec.rb +99 -24
- data/spec/rotate_spec.rb +62 -0
- data/zpng.gemspec +9 -5
- metadata +20 -4
- data/lib/zpng/readable_struct.rb +0 -56
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 45cbe6009082c09bd91263c35aafcde94fe9a244f30999a8181a42cde9d8c591
|
|
4
|
+
data.tar.gz: bfb0767ed14b07544ca7159454b9dbe5ccdd404f98f7e293dddb52b13abf43aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f3929af614a96f549e331a65c15f210bc96bd52c5d88625d85156eb507773b89fc010d968ceddd25de213124db0dc935821dc02f255555eff4780ff20b87eb91
|
|
7
|
+
data.tar.gz: 1d5ab750a6f0aa24fe710ee53b53240d0b3a4335e3484c6c24eb99419413acc3701af9bd7aa5c3928b6971344e0759ee1843e5dbeed09004d06906ef5f9d0a4c
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
GEM
|
|
2
2
|
remote: http://rubygems.org/
|
|
3
3
|
specs:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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.
|
|
10
|
-
|
|
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.
|
|
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
|
|
27
|
-
multipart-post (~> 2)
|
|
28
|
-
faraday-net_http (1.0.
|
|
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 (
|
|
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 (
|
|
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.
|
|
86
|
+
jwt (2.10.2)
|
|
87
|
+
base64
|
|
57
88
|
kamelcase (0.0.2)
|
|
58
89
|
semver2 (~> 3)
|
|
59
|
-
|
|
60
|
-
|
|
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.
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
106
|
+
process_executer (1.1.2)
|
|
107
|
+
psych (5.3.1)
|
|
108
|
+
date
|
|
73
109
|
stringio
|
|
74
|
-
public_suffix (
|
|
75
|
-
racc (1.
|
|
76
|
-
rack (3.
|
|
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.
|
|
79
|
-
rchardet (1.
|
|
80
|
-
rdoc (
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
158
|
+
2.4.22
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.4.
|
|
1
|
+
0.4.6
|
data/lib/zpng/bmp/reader.rb
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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 <
|
|
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
|
|
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
|
|
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] ||
|
|
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 ||
|
|
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
|
-
|
|
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 ==
|
|
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)/
|
|
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)/
|
|
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
|
-
#
|
|
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)
|
|
252
|
-
@g.send(op, c.g)
|
|
253
|
-
@b.send(op, c.b)
|
|
254
|
-
|
|
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)
|
|
259
|
-
@g.send(op)
|
|
260
|
-
@b.send(op)
|
|
261
|
-
|
|
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, :
|
|
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"
|
|
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(
|
|
158
|
-
if 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 -
|
|
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
|
|
491
|
-
|
|
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
|
-
|
|
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/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"])
|
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.
|
|
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:
|
|
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.
|
|
167
|
+
rubygems_version: 3.2.33
|
|
152
168
|
signing_key:
|
|
153
169
|
specification_version: 4
|
|
154
170
|
summary: pure ruby PNG file manipulation & validation
|
data/lib/zpng/readable_struct.rb
DELETED
|
@@ -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
|