image_size 3.4.0 → 3.6.0
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/.github/dependabot.yml +8 -0
- data/.github/workflows/check.yml +13 -19
- data/.github/workflows/rubocop.yml +4 -2
- data/.rubocop.yml +3 -0
- data/.rubocop_todo.yml +0 -6
- data/CHANGELOG.markdown +16 -3
- data/README.markdown +50 -3
- data/image_size.gemspec +15 -6
- data/lib/image_size/chunky_reader.rb +2 -2
- data/lib/image_size/isobmff.rb +2 -2
- data/lib/image_size/media_types.rb +1 -0
- data/lib/image_size/reader.rb +4 -3
- data/lib/image_size/seekable_io_reader.rb +4 -0
- data/lib/image_size/stream_io_reader.rb +6 -0
- data/lib/image_size/string_reader.rb +4 -0
- data/lib/image_size/uri_reader.rb +61 -10
- data/lib/image_size.rb +118 -29
- data/spec/image_size/chunky_reader_spec.rb +1 -1
- data/spec/image_size/seekable_io_reader_spec.rb +19 -24
- data/spec/image_size_spec.rb +263 -43
- data/spec/images/icns/16x12.icns +0 -0
- data/spec/images/icns/afp_scanpix_leta_pilipey_kyiv.256x256@1x.icns +0 -0
- data/spec/images/icns/reuters_scanpix_leta_garanich_kyiv.256x256@2x.icns +0 -0
- data/spec/images/icns/toc.512x512@1x.icns +0 -0
- data/spec/images/icns/toc.512x512@2x.icns +0 -0
- data/spec/images/svg/768b.430x430.svg +11 -0
- data/spec/test_server.rb +29 -3
- metadata +19 -10
data/lib/image_size.rb
CHANGED
|
@@ -31,19 +31,40 @@ class ImageSize
|
|
|
31
31
|
alias_method :h, :height
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
class << self
|
|
35
|
+
# Given path to image finds its format, width and height
|
|
36
|
+
def path(path)
|
|
37
|
+
new(Pathname.new(path))
|
|
38
|
+
end
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
# DPI used for svg and emf
|
|
41
|
+
def dpi
|
|
42
|
+
@dpi || 72.0
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Set DPI used for svg and emf
|
|
46
|
+
def dpi=(dpi)
|
|
47
|
+
fail ArgumentError, "dpi should be nil or positive, got #{dpi}" unless dpi.nil? || dpi > 0
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
@dpi = dpi ? dpi.to_f : nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Use display pixels instead of physical/image pixels for icns
|
|
53
|
+
attr_accessor :use_display_pixels
|
|
54
|
+
|
|
55
|
+
# Size of chunk to use by IO and URI readers
|
|
56
|
+
def chunk_size
|
|
57
|
+
@chunk_size || 4096
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Set size of chunk to use by IO and URI readers
|
|
61
|
+
def chunk_size=(chunk_size)
|
|
62
|
+
unless chunk_size.nil? || (chunk_size.is_a?(Integer) && chunk_size > 0)
|
|
63
|
+
fail ArgumentError, "chunk_size should be a positive Integer or nil, got #{chunk_size}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
@chunk_size = chunk_size
|
|
67
|
+
end
|
|
47
68
|
end
|
|
48
69
|
|
|
49
70
|
# Given image as any class responding to read and eof? or data as String, finds its format and dimensions
|
|
@@ -51,6 +72,7 @@ class ImageSize
|
|
|
51
72
|
Reader.open(data) do |ir|
|
|
52
73
|
@format = detect_format(ir)
|
|
53
74
|
@width, @height = send("size_of_#{@format}", ir) if @format
|
|
75
|
+
@byte_size = ir.byte_size
|
|
54
76
|
end
|
|
55
77
|
end
|
|
56
78
|
|
|
@@ -65,6 +87,8 @@ class ImageSize
|
|
|
65
87
|
attr_reader :height
|
|
66
88
|
alias_method :h, :height
|
|
67
89
|
|
|
90
|
+
attr_reader :byte_size
|
|
91
|
+
|
|
68
92
|
# get image width and height as an array which to_s method returns "#{width}x#{height}"
|
|
69
93
|
def size
|
|
70
94
|
Size.new([width, height]) if format
|
|
@@ -72,7 +96,7 @@ class ImageSize
|
|
|
72
96
|
|
|
73
97
|
# Media type (formerly known as a MIME type)
|
|
74
98
|
def media_type
|
|
75
|
-
|
|
99
|
+
media_types.first
|
|
76
100
|
end
|
|
77
101
|
|
|
78
102
|
# All media types:
|
|
@@ -87,6 +111,8 @@ private
|
|
|
87
111
|
|
|
88
112
|
SVG_R = /<svg\b([^>]*)>/.freeze
|
|
89
113
|
XML_R = /<\?xml|<!--/.freeze
|
|
114
|
+
private_constant :SVG_R, :XML_R
|
|
115
|
+
|
|
90
116
|
def detect_format(ir)
|
|
91
117
|
head = ir[0, 1024]
|
|
92
118
|
case
|
|
@@ -107,6 +133,7 @@ private
|
|
|
107
133
|
when head[0, 12] =~ /\ARIFF(?m:....)WEBP\z/ then :webp
|
|
108
134
|
when head[0, 4] == "\0\0\1\0" then :ico
|
|
109
135
|
when head[0, 4] == "\0\0\2\0" then :cur
|
|
136
|
+
when head[0, 4] == 'icns' then :icns
|
|
110
137
|
when head[0, 12] == "\0\0\0\fjP \r\n\207\n" then detect_jpeg2000_type(ir)
|
|
111
138
|
when head[0, 4] == "\377O\377Q" then :j2c
|
|
112
139
|
when head[0, 4] == "\1\0\0\0" && head[40, 4] == ' EMF' then :emf
|
|
@@ -153,13 +180,13 @@ private
|
|
|
153
180
|
end
|
|
154
181
|
|
|
155
182
|
def size_of_mng(ir)
|
|
156
|
-
|
|
183
|
+
fail FormatError, 'MHDR not in place for MNG' unless ir[12, 4] == 'MHDR'
|
|
157
184
|
|
|
158
185
|
ir.unpack(16, 8, 'NN')
|
|
159
186
|
end
|
|
160
187
|
|
|
161
188
|
def size_of_png(ir)
|
|
162
|
-
|
|
189
|
+
fail FormatError, 'IHDR not in place for PNG' unless ir[12, 4] == 'IHDR'
|
|
163
190
|
|
|
164
191
|
ir.unpack(16, 8, 'NN')
|
|
165
192
|
end
|
|
@@ -171,13 +198,15 @@ private
|
|
|
171
198
|
0xC9, 0xCA, 0xCB,
|
|
172
199
|
0xCD, 0xCE, 0xCF
|
|
173
200
|
].freeze
|
|
201
|
+
private_constant :JPEG_CODE_CHECK
|
|
202
|
+
|
|
174
203
|
def size_of_jpeg(ir)
|
|
175
204
|
section_marker = "\xFF"
|
|
176
205
|
offset = 2
|
|
177
206
|
loop do
|
|
178
207
|
offset += 1 until [nil, section_marker].include? ir[offset, 1]
|
|
179
208
|
offset += 1 until section_marker != ir[offset + 1, 1]
|
|
180
|
-
|
|
209
|
+
fail FormatError, 'EOF in JPEG' unless ir[offset, 1]
|
|
181
210
|
|
|
182
211
|
code, length = ir.unpack(offset, 4, 'xCn')
|
|
183
212
|
offset += 4
|
|
@@ -206,8 +235,7 @@ private
|
|
|
206
235
|
def size_of_ppm(ir)
|
|
207
236
|
header = ir[0, 1024]
|
|
208
237
|
header.gsub!(/^\#[^\n\r]*/m, '')
|
|
209
|
-
header
|
|
210
|
-
[$2.to_i, $3.to_i]
|
|
238
|
+
header.match(/^(?:P[1-6])\s+?(\d+)\s+?(\d+)/m)[1..2].map(&:to_i)
|
|
211
239
|
end
|
|
212
240
|
alias_method :size_of_pbm, :size_of_ppm
|
|
213
241
|
alias_method :size_of_pgm, :size_of_ppm
|
|
@@ -223,35 +251,37 @@ private
|
|
|
223
251
|
chunk = ir[offset, 32]
|
|
224
252
|
case chunk
|
|
225
253
|
when /\AWIDTH (\d+)\n/
|
|
226
|
-
width =
|
|
254
|
+
width = Regexp.last_match[1].to_i
|
|
227
255
|
when /\AHEIGHT (\d+)\n/
|
|
228
|
-
height =
|
|
256
|
+
height = Regexp.last_match[1].to_i
|
|
229
257
|
when /\AENDHDR\n/
|
|
230
258
|
break
|
|
231
259
|
when /\A(?:DEPTH|MAXVAL) \d+\n/, /\ATUPLTYPE \S+\n/
|
|
232
260
|
# ignore
|
|
233
261
|
else
|
|
234
|
-
|
|
262
|
+
fail FormatError, "Unexpected data in PAM header: #{chunk.inspect}"
|
|
235
263
|
end
|
|
236
|
-
offset +=
|
|
264
|
+
offset += Regexp.last_match[0].length
|
|
237
265
|
end
|
|
238
266
|
end
|
|
239
267
|
[width, height]
|
|
240
268
|
end
|
|
241
269
|
|
|
242
270
|
def size_of_xbm(ir)
|
|
243
|
-
ir[0, 1024]
|
|
244
|
-
[$1.to_i, $2.to_i]
|
|
271
|
+
ir[0, 1024].match(/^\#define\s*\S*\s*(\d+)\s*\n\#define\s*\S*\s*(\d+)/mi)[1..2].map(&:to_i)
|
|
245
272
|
end
|
|
246
273
|
|
|
247
274
|
def size_of_xpm(ir)
|
|
248
275
|
length = 1024
|
|
249
|
-
|
|
250
|
-
|
|
276
|
+
loop do
|
|
277
|
+
data = ir[0, length]
|
|
278
|
+
m = data.match(/"\s*(\d+)\s+(\d+)(?:\s+\d+\s+\d+){1,2}\s*"/m)
|
|
279
|
+
return m[1..2].map(&:to_i) if m
|
|
280
|
+
|
|
281
|
+
fail FormatError, 'XPM size not found' if data.length != length
|
|
251
282
|
|
|
252
283
|
length += 1024
|
|
253
284
|
end
|
|
254
|
-
[$1.to_i, $2.to_i]
|
|
255
285
|
end
|
|
256
286
|
|
|
257
287
|
def size_of_psd(ir)
|
|
@@ -271,7 +301,7 @@ private
|
|
|
271
301
|
width = height = nil
|
|
272
302
|
until width && height
|
|
273
303
|
ifd = ir.fetch(offset, 12)
|
|
274
|
-
|
|
304
|
+
fail FormatError, 'Reached end of directory entries in TIFF' if offset > num_dirent
|
|
275
305
|
|
|
276
306
|
tag, type = ifd.unpack(endian2b * 2)
|
|
277
307
|
offset += 12
|
|
@@ -331,6 +361,60 @@ private
|
|
|
331
361
|
end
|
|
332
362
|
alias_method :size_of_cur, :size_of_ico
|
|
333
363
|
|
|
364
|
+
ICNS_16X12 = %w[icm# icm4 icm8].freeze
|
|
365
|
+
ICNS_SQUARE = {
|
|
366
|
+
[16, 1] => %w[ic04 icp4 ics# ics4 ics8 is32 s8mk],
|
|
367
|
+
[16, 2] => %w[ic11],
|
|
368
|
+
[18, 1] => %w[icsb],
|
|
369
|
+
[18, 2] => %w[icsB],
|
|
370
|
+
[24, 1] => %w[sb24],
|
|
371
|
+
[24, 2] => %w[SB24],
|
|
372
|
+
[32, 1] => %w[ICN# ICON ic05 icl4 icl8 icp5 il32 l8mk],
|
|
373
|
+
[32, 2] => %w[ic12],
|
|
374
|
+
[48, 1] => %w[h8mk ich# ich4 ich8 icp6 ih32],
|
|
375
|
+
[128, 1] => %w[ic07 it32 t8mk],
|
|
376
|
+
[128, 2] => %w[ic13],
|
|
377
|
+
[256, 1] => %w[ic08],
|
|
378
|
+
[256, 2] => %w[ic14],
|
|
379
|
+
[512, 1] => %w[ic09],
|
|
380
|
+
[512, 2] => %w[ic10],
|
|
381
|
+
}.each_with_object({}){ |(spec, types), h| types.each{ |type| h[type] = spec } }.freeze
|
|
382
|
+
private_constant :ICNS_16X12, :ICNS_SQUARE
|
|
383
|
+
|
|
384
|
+
def size_of_icns(ir)
|
|
385
|
+
file_length = ir.unpack1(4, 4, 'N')
|
|
386
|
+
offset = 8
|
|
387
|
+
|
|
388
|
+
types = []
|
|
389
|
+
while offset < file_length
|
|
390
|
+
type = ir[offset, 4]
|
|
391
|
+
length = ir.unpack1(offset + 4, 4, 'N')
|
|
392
|
+
|
|
393
|
+
case type
|
|
394
|
+
when 'TOC ' # rely on table of contents
|
|
395
|
+
fail FormatError, "TOC length #{length} is not divisible by 8" unless length % 8 == 0
|
|
396
|
+
|
|
397
|
+
types = (1...(length / 8)).map{ |i| ir[offset + (8 * i), 4] }
|
|
398
|
+
break
|
|
399
|
+
when 'icnV', 'info', 'name' # not icons
|
|
400
|
+
when 'slct', 'sbtp', "\375\331/\250" # nested icns
|
|
401
|
+
else
|
|
402
|
+
types << type
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
offset += length
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
types.map do |type|
|
|
409
|
+
next [16, 12] if ICNS_16X12.include?(type)
|
|
410
|
+
|
|
411
|
+
side, scale = ICNS_SQUARE[type]
|
|
412
|
+
fail FormatError, "unknown icon type: #{type.inspect}" unless side
|
|
413
|
+
|
|
414
|
+
(self.class.use_display_pixels ? [side] : [side * scale]) * 2
|
|
415
|
+
end.max
|
|
416
|
+
end
|
|
417
|
+
|
|
334
418
|
def size_of_webp(ir)
|
|
335
419
|
case ir.fetch(12, 4)
|
|
336
420
|
when 'VP8 '
|
|
@@ -348,6 +432,8 @@ private
|
|
|
348
432
|
recurse: %w[jp2h],
|
|
349
433
|
last: %w[jp2h]
|
|
350
434
|
)
|
|
435
|
+
private_constant :JP2_WALKER
|
|
436
|
+
|
|
351
437
|
def size_of_jp2(ir)
|
|
352
438
|
JP2_WALKER.recurse(ir) do |box|
|
|
353
439
|
return ir.unpack(box.data_offset, 8, 'NN').reverse if box.type == 'ihdr'
|
|
@@ -361,6 +447,7 @@ private
|
|
|
361
447
|
|
|
362
448
|
EMF_UMAX = 256**4
|
|
363
449
|
EMF_SMAX = EMF_UMAX / 2
|
|
450
|
+
private_constant :EMF_UMAX, :EMF_SMAX
|
|
364
451
|
|
|
365
452
|
def size_of_emf(ir)
|
|
366
453
|
left, top, right, bottom =
|
|
@@ -380,6 +467,8 @@ private
|
|
|
380
467
|
full: %w[meta hdlr pitm ipma ispe],
|
|
381
468
|
last: %w[meta]
|
|
382
469
|
)
|
|
470
|
+
private_constant :HEIF_WALKER
|
|
471
|
+
|
|
383
472
|
def size_of_heif(ir)
|
|
384
473
|
pitm = nil
|
|
385
474
|
ipma = nil
|
|
@@ -390,11 +479,11 @@ private
|
|
|
390
479
|
HEIF_WALKER.recurse(ir) do |box, _path|
|
|
391
480
|
case box.type
|
|
392
481
|
when 'hdlr'
|
|
393
|
-
|
|
482
|
+
fail FormatError, "hdlr box too small (#{box.data_size})" if box.data_size < 8
|
|
394
483
|
|
|
395
484
|
return nil unless ir[box.data_offset + 4, 4] == 'pict'
|
|
396
485
|
when 'pitm'
|
|
397
|
-
|
|
486
|
+
fail FormatError, 'second pitm box encountered' if pitm
|
|
398
487
|
|
|
399
488
|
pitm = box.version == 0 ? ir.unpack1(box.data_offset, 2, 'n') : ir.unpack1(box.data_offset, 4, 'N')
|
|
400
489
|
when 'ipma'
|
|
@@ -2,49 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
require 'rspec'
|
|
4
4
|
|
|
5
|
-
require 'image_size
|
|
5
|
+
require 'image_size'
|
|
6
6
|
|
|
7
7
|
describe ImageSize::SeekableIOReader do
|
|
8
8
|
context :[] do
|
|
9
|
-
def
|
|
10
|
-
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def io
|
|
14
|
-
File.open('GPL', 'rb').tap do |io|
|
|
15
|
-
ios << io
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
after do
|
|
20
|
-
ios.pop.close until ios.empty?
|
|
9
|
+
def new_io(&block)
|
|
10
|
+
File.open('GPL', 'rb', &block)
|
|
21
11
|
end
|
|
22
12
|
|
|
23
13
|
def new_reader
|
|
24
|
-
|
|
14
|
+
new_io do |io|
|
|
15
|
+
yield ImageSize::SeekableIOReader.new(io)
|
|
16
|
+
end
|
|
25
17
|
end
|
|
26
18
|
|
|
27
|
-
let(:content){
|
|
19
|
+
let(:content){ new_io(&:read) }
|
|
28
20
|
|
|
29
21
|
it 'reads as expected when pieces are read consecutively' do
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
new_reader do |reader|
|
|
23
|
+
0.step(content.length + 4096, 100) do |offset|
|
|
24
|
+
expect(reader[offset, 100]).to eq(content[offset, 100])
|
|
25
|
+
end
|
|
33
26
|
end
|
|
34
27
|
end
|
|
35
28
|
|
|
36
29
|
it 'reads as expected when pieces are read backwards' do
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
new_reader do |reader|
|
|
31
|
+
(content.length + 4096).step(0, -100) do |offset|
|
|
32
|
+
expect(reader[offset, 100]).to eq(content[offset, 100])
|
|
33
|
+
end
|
|
40
34
|
end
|
|
41
35
|
end
|
|
42
36
|
|
|
43
37
|
it 'reads as expected when pieces are read in random order' do
|
|
44
38
|
100.times do
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
new_reader do |reader|
|
|
40
|
+
0.step(content.length + 4096, 100).to_a.shuffle.each do |offset|
|
|
41
|
+
expect(reader[offset, 100]).to eq(content[offset, 100])
|
|
42
|
+
end
|
|
48
43
|
end
|
|
49
44
|
end
|
|
50
45
|
end
|