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.
data/lib/image_size.rb CHANGED
@@ -31,19 +31,40 @@ class ImageSize
31
31
  alias_method :h, :height
32
32
  end
33
33
 
34
- # Given path to image finds its format, width and height
35
- def self.path(path)
36
- new(Pathname.new(path))
37
- end
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
- # Used for svg
40
- def self.dpi
41
- @dpi || 72
42
- end
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
- # Used for svg
45
- def self.dpi=(dpi)
46
- @dpi = dpi.to_f
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
- MEDIA_TYPES.fetch(format, []).first
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
- raise FormatError, 'MHDR not in place for MNG' unless ir[12, 4] == 'MHDR'
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
- raise FormatError, 'IHDR not in place for PNG' unless ir[12, 4] == 'IHDR'
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
- raise FormatError, 'EOF in JPEG' unless ir[offset, 1]
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 =~ /^(P[1-6])\s+?(\d+)\s+?(\d+)/m
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 = $1.to_i
254
+ width = Regexp.last_match[1].to_i
227
255
  when /\AHEIGHT (\d+)\n/
228
- height = $1.to_i
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
- raise FormatError, "Unexpected data in PAM header: #{chunk.inspect}"
262
+ fail FormatError, "Unexpected data in PAM header: #{chunk.inspect}"
235
263
  end
236
- offset += $&.length
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] =~ /^\#define\s*\S*\s*(\d+)\s*\n\#define\s*\S*\s*(\d+)/mi
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
- until (data = ir[0, length]) =~ /"\s*(\d+)\s+(\d+)(\s+\d+\s+\d+){1,2}\s*"/m
250
- raise FormatError, 'XPM size not found' if data.length != length
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
- raise FormatError, 'Reached end of directory entries in TIFF' if offset > num_dirent
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
- raise FormatError, "hdlr box too small (#{box.data_size})" if box.data_size < 8
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
- raise FormatError, 'second pitm box encountered' if pitm
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,7 +2,7 @@
2
2
 
3
3
  require 'rspec'
4
4
 
5
- require 'image_size/chunky_reader'
5
+ require 'image_size'
6
6
 
7
7
  describe ImageSize::ChunkyReader do
8
8
  context :[] do
@@ -2,49 +2,44 @@
2
2
 
3
3
  require 'rspec'
4
4
 
5
- require 'image_size/seekable_io_reader'
5
+ require 'image_size'
6
6
 
7
7
  describe ImageSize::SeekableIOReader do
8
8
  context :[] do
9
- def ios
10
- @ios ||= []
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
- ImageSize::SeekableIOReader.new(io)
14
+ new_io do |io|
15
+ yield ImageSize::SeekableIOReader.new(io)
16
+ end
25
17
  end
26
18
 
27
- let(:content){ io.read }
19
+ let(:content){ new_io(&:read) }
28
20
 
29
21
  it 'reads as expected when pieces are read consecutively' do
30
- reader = new_reader
31
- 0.step(content.length + 4096, 100) do |offset|
32
- expect(reader[offset, 100]).to eq(content[offset, 100])
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
- reader = new_reader
38
- (content.length + 4096).step(0, -100) do |offset|
39
- expect(reader[offset, 100]).to eq(content[offset, 100])
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
- reader = new_reader
46
- 0.step(content.length + 4096, 100).to_a.shuffle.each do |offset|
47
- expect(reader[offset, 100]).to eq(content[offset, 100])
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