image_size 3.2.0 → 3.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/check.yml +11 -9
- data/.github/workflows/rubocop.yml +2 -2
- data/.rubocop.yml +2 -32
- data/CHANGELOG.markdown +12 -0
- data/README.markdown +9 -8
- data/image_size.gemspec +5 -3
- data/lib/image_size/format_error.rb +5 -0
- data/lib/image_size/isobmff.rb +99 -0
- data/lib/image_size/media_types.rb +32 -0
- data/lib/image_size/reader.rb +17 -0
- data/lib/image_size/uri.rb +3 -0
- data/lib/image_size/uri_reader.rb +1 -0
- data/lib/image_size.rb +99 -54
- data/spec/image_size/chunky_reader_spec.rb +1 -1
- data/spec/image_size/isobmff_spec.rb +352 -0
- data/spec/image_size_spec.rb +53 -17
- data/spec/images/heif/ap_maloletka_mariupol.452x301.heic +0 -0
- data/spec/images/heif/maxar_mariupol.400x300.avif +0 -0
- data/spec/images/heif/multiple.169x83.heic +0 -0
- data/spec/images/heif/rotate.73x173.avif +0 -0
- data/spec/images/heif/sequence.7x5.avif +0 -0
- data/spec/test_server.rb +7 -6
- metadata +25 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d88e4be6d69bedc0f976c9b73544a20ac08ab66d6b44ce1061d1374f9d2a9dc8
|
4
|
+
data.tar.gz: 5b2986943b8bbeb339d0c31d0a98c5847a1a86141d5e754adaf0bd01e3d8d60f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1b75df7c37cbcca0886eade3e47b082cde0468331c954d0605130cba564bb7718d777628599fea7332e2ae1ef9f15b80ce77072352a109dadbf9749fea049572
|
7
|
+
data.tar.gz: ffc57b47280776fc7be3cb2866699f32f90a2e036b2dca703ac4262aa77750032ffb54700437958ba7b4948c934d5ea6e41f2acad4e7e71ddec84a98f18edabf
|
data/.github/workflows/check.yml
CHANGED
@@ -10,9 +10,6 @@ jobs:
|
|
10
10
|
strategy:
|
11
11
|
matrix:
|
12
12
|
ruby:
|
13
|
-
- '2.0'
|
14
|
-
- '2.1'
|
15
|
-
- '2.2'
|
16
13
|
- '2.3'
|
17
14
|
- '2.4'
|
18
15
|
- '2.5'
|
@@ -20,12 +17,13 @@ jobs:
|
|
20
17
|
- '2.7'
|
21
18
|
- '3.0'
|
22
19
|
- '3.1'
|
23
|
-
-
|
24
|
-
-
|
20
|
+
- '3.2'
|
21
|
+
- '3.3'
|
25
22
|
- jruby-9.3
|
23
|
+
- jruby-9.4
|
26
24
|
fail-fast: false
|
27
25
|
steps:
|
28
|
-
- uses: actions/checkout@
|
26
|
+
- uses: actions/checkout@v3
|
29
27
|
- uses: ruby/setup-ruby@v1
|
30
28
|
with:
|
31
29
|
ruby-version: "${{ matrix.ruby }}"
|
@@ -37,11 +35,13 @@ jobs:
|
|
37
35
|
strategy:
|
38
36
|
matrix:
|
39
37
|
container:
|
40
|
-
- rspec/ci:1.8.7
|
41
38
|
- rspec/ci:1.9.3
|
39
|
+
- ruby:2.0
|
40
|
+
- ruby:2.1
|
41
|
+
- ruby:2.2
|
42
42
|
fail-fast: false
|
43
43
|
steps:
|
44
|
-
- uses: actions/checkout@
|
44
|
+
- uses: actions/checkout@v3
|
45
45
|
- run: bundle install
|
46
46
|
- run: bundle exec rspec --format documentation
|
47
47
|
windows:
|
@@ -53,9 +53,11 @@ jobs:
|
|
53
53
|
- '2.7'
|
54
54
|
- '3.0'
|
55
55
|
- '3.1'
|
56
|
+
- '3.2'
|
57
|
+
- '3.3'
|
56
58
|
fail-fast: false
|
57
59
|
steps:
|
58
|
-
- uses: actions/checkout@
|
60
|
+
- uses: actions/checkout@v3
|
59
61
|
- uses: ruby/setup-ruby@v1
|
60
62
|
with:
|
61
63
|
ruby-version: "${{ matrix.ruby }}"
|
data/.rubocop.yml
CHANGED
@@ -15,9 +15,6 @@ Layout/CaseIndentation:
|
|
15
15
|
Layout/EndAlignment:
|
16
16
|
EnforcedStyleAlignWith: variable
|
17
17
|
|
18
|
-
Layout/FirstHashElementIndentation:
|
19
|
-
EnforcedStyle: consistent
|
20
|
-
|
21
18
|
Layout/LineLength:
|
22
19
|
Max: 120
|
23
20
|
|
@@ -25,9 +22,6 @@ Layout/SpaceBeforeBlockBraces:
|
|
25
22
|
EnforcedStyle: no_space
|
26
23
|
EnforcedStyleForEmptyBraces: no_space
|
27
24
|
|
28
|
-
Lint/PercentStringArray: # broken in rubocop 0.55.0
|
29
|
-
Enabled: false
|
30
|
-
|
31
25
|
Metrics/BlockLength:
|
32
26
|
Exclude:
|
33
27
|
- 'spec/**/*.rb'
|
@@ -41,42 +35,18 @@ Metrics/MethodLength:
|
|
41
35
|
Naming/MethodParameterName:
|
42
36
|
Enabled: false
|
43
37
|
|
44
|
-
Style/AccessorGrouping:
|
45
|
-
Enabled: false
|
46
|
-
|
47
38
|
Style/Alias:
|
48
39
|
EnforcedStyle: prefer_alias_method
|
49
40
|
|
50
|
-
Style/
|
51
|
-
Enabled: false
|
52
|
-
|
53
|
-
Style/Encoding:
|
54
|
-
Enabled: false
|
55
|
-
|
56
|
-
Style/FileRead:
|
41
|
+
Style/ArgumentsForwarding:
|
57
42
|
Enabled: false
|
58
43
|
|
59
|
-
Style/
|
60
|
-
Enabled: true
|
61
|
-
|
62
|
-
Style/HashSyntax:
|
63
|
-
EnforcedStyle: hash_rockets
|
64
|
-
|
65
|
-
Style/HashTransformKeys:
|
66
|
-
Enabled: false
|
67
|
-
|
68
|
-
Style/HashTransformValues:
|
69
|
-
Enabled: false
|
70
|
-
|
71
|
-
Style/IfUnlessModifier:
|
44
|
+
Style/EmptyCaseCondition:
|
72
45
|
Enabled: false
|
73
46
|
|
74
47
|
Style/NumericPredicate:
|
75
48
|
Enabled: false
|
76
49
|
|
77
|
-
Style/ParallelAssignment:
|
78
|
-
Enabled: false
|
79
|
-
|
80
50
|
Style/SafeNavigation:
|
81
51
|
Enabled: false
|
82
52
|
|
data/CHANGELOG.markdown
CHANGED
@@ -2,6 +2,18 @@
|
|
2
2
|
|
3
3
|
## unreleased
|
4
4
|
|
5
|
+
## v3.4.0 (2024-01-16)
|
6
|
+
|
7
|
+
* Provide access to media types using media_type and media_types methods [#22](https://github.com/toy/image_size/issues/22) [@toy](https://github.com/toy)
|
8
|
+
* Allow fetching from HTTP server by requiring image_size/uri [@toy](https://github.com/toy)
|
9
|
+
* Fix for ArgumentError when requiring only image_size/uri_reader (without image_size) [@toy](https://github.com/toy)
|
10
|
+
* Require ruby 1.9.3 [@toy](https://github.com/toy)
|
11
|
+
|
12
|
+
## v3.3.0 (2023-05-30)
|
13
|
+
|
14
|
+
* Support `HEIF` (`HEIC` and `AVIF`) images [#19](https://github.com/toy/image_size/issues/19) [@toy](https://github.com/toy)
|
15
|
+
* Fix handling `JPEG 2000` 64 bit size boxes [@toy](https://github.com/toy)
|
16
|
+
|
5
17
|
## v3.2.0 (2022-11-03)
|
6
18
|
|
7
19
|
* Support `EMF` images [#21](https://github.com/toy/image_size/pull/21) [@opoudjis](https://github.com/opoudjis)
|
data/README.markdown
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
[![Gem Version](https://img.shields.io/gem/v/image_size?logo=rubygems)](https://rubygems.org/gems/image_size)
|
2
|
-
[![Build Status](https://img.shields.io/github/workflow/status/toy/image_size/check
|
3
|
-
[![Rubocop](https://img.shields.io/github/workflow/status/toy/image_size/rubocop
|
2
|
+
[![Build Status](https://img.shields.io/github/actions/workflow/status/toy/image_size/check.yml?logo=github)](https://github.com/toy/image_size/actions/workflows/check.yml)
|
3
|
+
[![Rubocop](https://img.shields.io/github/actions/workflow/status/toy/image_size/rubocop.yml?label=rubocop&logo=rubocop)](https://github.com/toy/image_size/actions/workflows/rubocop.yml)
|
4
4
|
|
5
5
|
# image_size
|
6
6
|
|
7
|
-
Measure image size using pure Ruby.
|
8
|
-
Formats: `apng`, `bmp`, `cur`, `emf`, `gif`, `ico`, `j2c`, `jp2`, `jpeg`, `jpx`, `mng`, `pam`, `pbm`, `pcx`, `pgm`, `png`, `ppm`, `psd`, `svg`, `swf`, `tiff`, `webp`, `xbm`, `xpm`.
|
7
|
+
Measure image size/dimensions using pure Ruby.
|
8
|
+
Formats: `apng`, `avif`, `bmp`, `cur`, `emf`, `gif`, `heic`, `heif`, `ico`, `j2c`, `jp2`, `jpeg`, `jpx`, `mng`, `pam`, `pbm`, `pcx`, `pgm`, `png`, `ppm`, `psd`, `svg`, `swf`, `tiff`, `webp`, `xbm`, `xpm`.
|
9
9
|
|
10
10
|
## Installation
|
11
11
|
|
@@ -26,7 +26,7 @@ gem 'image_size', '~> 3.0'
|
|
26
26
|
```ruby
|
27
27
|
image_size = ImageSize.path('spec/images/jpeg/436x429.jpeg')
|
28
28
|
|
29
|
-
image_size.format #=> :
|
29
|
+
image_size.format #=> :jpeg
|
30
30
|
image_size.width #=> 436
|
31
31
|
image_size.height #=> 429
|
32
32
|
image_size.w #=> 436
|
@@ -38,6 +38,8 @@ image_size.size.width #=> 436
|
|
38
38
|
image_size.size.height #=> 429
|
39
39
|
image_size.size.w #=> 436
|
40
40
|
image_size.size.h #=> 429
|
41
|
+
image_size.media_type #=> "image/jpeg"
|
42
|
+
image_size.media_types #=> ["image/jpeg"]
|
41
43
|
```
|
42
44
|
|
43
45
|
Or using `IO` object:
|
@@ -96,8 +98,7 @@ If server recognises Range header, only needed chunks will be fetched even for T
|
|
96
98
|
of data will be fetched, in most cases first few kilobytes (TIFF images is an exception).
|
97
99
|
|
98
100
|
```ruby
|
99
|
-
require 'image_size'
|
100
|
-
require 'image_size/uri_reader'
|
101
|
+
require 'image_size/uri'
|
101
102
|
|
102
103
|
url = 'http://upload.wikimedia.org/wikipedia/commons/b/b4/Mardin_1350660_1350692_33_images.jpg'
|
103
104
|
p ImageSize.url(url).size
|
@@ -144,4 +145,4 @@ puts Benchmark.measure{ p ImageSize.url(url).size }
|
|
144
145
|
This code is free to use under the terms of the [Ruby's licence](LICENSE.txt).
|
145
146
|
|
146
147
|
Original author: Keisuke Minami <keisuke@rccn.com>.\
|
147
|
-
Further development 2010-
|
148
|
+
Further development 2010-2024 Ivan Kuchin https://github.com/toy/image_size
|
data/image_size.gemspec
CHANGED
@@ -2,13 +2,15 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = 'image_size'
|
5
|
-
s.version = '3.
|
6
|
-
s.summary = %q{Measure image size using pure Ruby}
|
7
|
-
s.description = %q{Measure following file dimensions: apng, bmp, cur, emf, gif, ico, j2c, jp2, jpeg, jpx, mng, pam, pbm, pcx, pgm, png, ppm, psd, svg, swf, tiff, webp, xbm, xpm}
|
5
|
+
s.version = '3.4.0'
|
6
|
+
s.summary = %q{Measure image size/dimensions using pure Ruby}
|
7
|
+
s.description = %q{Measure following file dimensions: apng, avif, bmp, cur, emf, gif, heic, heif, ico, j2c, jp2, jpeg, jpx, mng, pam, pbm, pcx, pgm, png, ppm, psd, svg, swf, tiff, webp, xbm, xpm}
|
8
8
|
s.homepage = "https://github.com/toy/#{s.name}"
|
9
9
|
s.authors = ['Keisuke Minami', 'Ivan Kuchin']
|
10
10
|
s.license = 'Ruby'
|
11
11
|
|
12
|
+
s.required_ruby_version = '>= 1.9.3'
|
13
|
+
|
12
14
|
s.metadata = {
|
13
15
|
'bug_tracker_uri' => "https://github.com/toy/#{s.name}/issues",
|
14
16
|
'changelog_uri' => "https://github.com/toy/#{s.name}/blob/master/CHANGELOG.markdown",
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'image_size/format_error'
|
4
|
+
|
5
|
+
require 'set'
|
6
|
+
|
7
|
+
class ImageSize
|
8
|
+
class ISOBMFF # :nodoc:
|
9
|
+
class Box # :nodoc:
|
10
|
+
attr_reader :type, :offset, :size, :relative_data_offset, :index
|
11
|
+
|
12
|
+
def initialize(attributes)
|
13
|
+
@type = attributes.fetch(:type)
|
14
|
+
@offset = attributes.fetch(:offset)
|
15
|
+
@size = attributes.fetch(:size) == 0 ? nil : attributes[:size]
|
16
|
+
@relative_data_offset = attributes.fetch(:relative_data_offset)
|
17
|
+
@index = attributes.fetch(:index)
|
18
|
+
end
|
19
|
+
|
20
|
+
def data_offset
|
21
|
+
offset + relative_data_offset
|
22
|
+
end
|
23
|
+
|
24
|
+
def data_size
|
25
|
+
size ? size - relative_data_offset : nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class FullBox < Box # :nodoc:
|
30
|
+
attr_reader :version, :flags
|
31
|
+
|
32
|
+
def initialize(attributes)
|
33
|
+
super
|
34
|
+
@version = attributes.fetch(:version)
|
35
|
+
@flags = attributes.fetch(:flags)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
S64_OVERFLOW = 1 << 63
|
40
|
+
|
41
|
+
def initialize(options = {})
|
42
|
+
@full = options.fetch(:full, []).to_set
|
43
|
+
@last = options.fetch(:last, []).to_set
|
44
|
+
@recurse = options.fetch(:recurse, []).to_set
|
45
|
+
end
|
46
|
+
|
47
|
+
def walk(reader, offset = 0, length = nil)
|
48
|
+
max_offset = length ? offset + length : S64_OVERFLOW
|
49
|
+
index = 1
|
50
|
+
while offset < max_offset && !['', nil].include?(reader[offset, 4])
|
51
|
+
size = reader.unpack1(offset, 4, 'N')
|
52
|
+
type = reader.fetch(offset + 4, 4)
|
53
|
+
relative_data_offset = 8
|
54
|
+
|
55
|
+
case size
|
56
|
+
when 1
|
57
|
+
size = reader.unpack1(offset + 8, 8, 'Q>')
|
58
|
+
relative_data_offset += 8
|
59
|
+
raise FormatError, "Unexpected ISOBMFF xl-box size #{size}" if size < 16
|
60
|
+
when 2..7
|
61
|
+
raise FormatError, "Reserved ISOBMFF box size #{size}"
|
62
|
+
end
|
63
|
+
|
64
|
+
attributes = {
|
65
|
+
type: type,
|
66
|
+
offset: offset,
|
67
|
+
size: size,
|
68
|
+
relative_data_offset: relative_data_offset,
|
69
|
+
index: index,
|
70
|
+
}
|
71
|
+
|
72
|
+
if @full.include?(type)
|
73
|
+
version_n_flags = reader.unpack1(offset + relative_data_offset, 4, 'N')
|
74
|
+
attributes[:version] = version_n_flags >> 24
|
75
|
+
attributes[:flags] = version_n_flags & 0xffffff
|
76
|
+
|
77
|
+
attributes[:relative_data_offset] += 4
|
78
|
+
|
79
|
+
yield FullBox.new(attributes)
|
80
|
+
else
|
81
|
+
yield Box.new(attributes)
|
82
|
+
end
|
83
|
+
|
84
|
+
break if size == 0 || @last.include?(type)
|
85
|
+
|
86
|
+
index += 1
|
87
|
+
offset += size
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def recurse(reader, offset = 0, length = nil, &block)
|
92
|
+
walk(reader, offset, length) do |box|
|
93
|
+
yield box
|
94
|
+
|
95
|
+
recurse(reader, box.data_offset, box.data_size, &block) if @recurse.include?(box.type)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ImageSize
|
4
|
+
MEDIA_TYPES = {
|
5
|
+
apng: %w[image/apng image/vnd.mozilla.apng],
|
6
|
+
avif: %w[image/avif],
|
7
|
+
bmp: %w[image/bmp],
|
8
|
+
cur: %w[image/vnd.microsoft.icon],
|
9
|
+
emf: %w[image/emf],
|
10
|
+
gif: %w[image/gif],
|
11
|
+
heic: %w[image/heic image/heif],
|
12
|
+
ico: %w[image/x-icon image/vnd.microsoft.icon],
|
13
|
+
j2c: %w[image/j2c],
|
14
|
+
jp2: %w[image/jp2],
|
15
|
+
jpeg: %w[image/jpeg],
|
16
|
+
jpx: %w[image/jpx],
|
17
|
+
mng: %w[video/x-mng image/x-mng],
|
18
|
+
pam: %w[image/x-portable-arbitrarymap],
|
19
|
+
pbm: %w[image/x-portable-bitmap image/x-portable-anymap],
|
20
|
+
pcx: %w[image/x-pcx image/vnd.zbrush.pcx],
|
21
|
+
pgm: %w[image/x-portable-graymap image/x-portable-anymap],
|
22
|
+
png: %w[image/png],
|
23
|
+
ppm: %w[image/x-portable-pixmap image/x-portable-anymap],
|
24
|
+
psd: %w[image/vnd.adobe.photoshop],
|
25
|
+
svg: %w[image/svg+xml],
|
26
|
+
swf: %w[application/x-shockwave-flash application/vnd.adobe.flash.movie],
|
27
|
+
tiff: %w[image/tiff],
|
28
|
+
webp: %w[image/webp],
|
29
|
+
xbm: %w[image/x-xbitmap],
|
30
|
+
xpm: %w[image/x-xpixmap],
|
31
|
+
}.freeze
|
32
|
+
end
|
data/lib/image_size/reader.rb
CHANGED
@@ -5,6 +5,19 @@ require 'stringio'
|
|
5
5
|
|
6
6
|
class ImageSize
|
7
7
|
module Reader # :nodoc:
|
8
|
+
class Stream # :nodoc:
|
9
|
+
def initialize(reader, offset)
|
10
|
+
@reader = reader
|
11
|
+
@offset = offset
|
12
|
+
end
|
13
|
+
|
14
|
+
def unpack1(length, format)
|
15
|
+
result = @reader.unpack1(@offset, length, format)
|
16
|
+
@offset += length
|
17
|
+
result
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
8
21
|
class << self
|
9
22
|
def open(input)
|
10
23
|
case
|
@@ -60,5 +73,9 @@ class ImageSize
|
|
60
73
|
fetch(offset, length).unpack(format)[0]
|
61
74
|
end
|
62
75
|
end
|
76
|
+
|
77
|
+
def stream(offset)
|
78
|
+
Stream.new(self, offset)
|
79
|
+
end
|
63
80
|
end
|
64
81
|
end
|
data/lib/image_size.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
# encoding: BINARY
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require 'image_size/isobmff'
|
5
|
+
require 'image_size/format_error'
|
6
|
+
require 'image_size/media_types'
|
4
7
|
require 'image_size/reader'
|
5
8
|
require 'image_size/seekable_io_reader'
|
6
9
|
require 'image_size/stream_io_reader'
|
@@ -8,8 +11,6 @@ require 'image_size/string_reader'
|
|
8
11
|
|
9
12
|
# Determine image format and size
|
10
13
|
class ImageSize
|
11
|
-
class FormatError < StandardError; end
|
12
|
-
|
13
14
|
# Array joining with 'x'
|
14
15
|
class Size < Array
|
15
16
|
# join using 'x'
|
@@ -69,6 +70,19 @@ class ImageSize
|
|
69
70
|
Size.new([width, height]) if format
|
70
71
|
end
|
71
72
|
|
73
|
+
# Media type (formerly known as a MIME type)
|
74
|
+
def media_type
|
75
|
+
MEDIA_TYPES.fetch(format, []).first
|
76
|
+
end
|
77
|
+
|
78
|
+
# All media types:
|
79
|
+
# * commonly used and official like for apng and ico
|
80
|
+
# * main and compatible like for heic and pnm (pbm, pgm, ppm)
|
81
|
+
# * multiple unregistered like for mng
|
82
|
+
def media_types
|
83
|
+
MEDIA_TYPES.fetch(format, [])
|
84
|
+
end
|
85
|
+
|
72
86
|
private
|
73
87
|
|
74
88
|
SVG_R = /<svg\b([^>]*)>/.freeze
|
@@ -77,25 +91,27 @@ private
|
|
77
91
|
head = ir[0, 1024]
|
78
92
|
case
|
79
93
|
when head.nil? || head.empty? then nil
|
80
|
-
when head[0, 6] =~
|
94
|
+
when head[0, 6] =~ /\AGIF8[79]a\z/ then :gif
|
81
95
|
when head[0, 8] == "\211PNG\r\n\032\n" then detect_png_type(ir)
|
82
96
|
when head[0, 8] == "\212MNG\r\n\032\n" then :mng
|
83
97
|
when head[0, 2] == "\377\330" then :jpeg
|
84
98
|
when head[0, 2] == 'BM' then :bmp
|
85
|
-
when head[0, 3] =~
|
99
|
+
when head[0, 3] =~ /\AP([1-6]\s|7\n)\z/ then detect_pnm_type(ir)
|
86
100
|
when head =~ /\#define\s+\S+\s+\d+/ then :xbm
|
87
101
|
when %W[II*\0 MM\0*].include?(head[0, 4]) then :tiff
|
88
102
|
when head =~ %r{/\* XPM \*/} then :xpm
|
89
103
|
when head[0, 4] == '8BPS' then :psd
|
90
|
-
when head[0, 3] =~
|
104
|
+
when head[0, 3] =~ /\A[FC]WS\z/ then :swf
|
91
105
|
when head =~ SVG_R || (head =~ XML_R && ir[0, 4096] =~ SVG_R) then :svg
|
92
106
|
when head[0, 2] =~ /\n[\0-\5]/ then :pcx
|
93
|
-
when head[0, 12] =~
|
107
|
+
when head[0, 12] =~ /\ARIFF(?m:....)WEBP\z/ then :webp
|
94
108
|
when head[0, 4] == "\0\0\1\0" then :ico
|
95
109
|
when head[0, 4] == "\0\0\2\0" then :cur
|
96
110
|
when head[0, 12] == "\0\0\0\fjP \r\n\207\n" then detect_jpeg2000_type(ir)
|
97
111
|
when head[0, 4] == "\377O\377Q" then :j2c
|
98
112
|
when head[0, 4] == "\1\0\0\0" && head[40, 4] == ' EMF' then :emf
|
113
|
+
when head[4, 8] =~ /\Aftypavi[fs]\z/ then :avif
|
114
|
+
when head[4, 8] =~ /\Aftyp(hei[cs]|mif[12]|msf1)\z/ then :heic
|
99
115
|
end
|
100
116
|
end
|
101
117
|
|
@@ -107,7 +123,7 @@ private
|
|
107
123
|
return :apng if type == 'acTL'
|
108
124
|
|
109
125
|
length = ir.unpack1(offset, 4, 'N')
|
110
|
-
offset +=
|
126
|
+
offset += length + 8 + 4
|
111
127
|
end
|
112
128
|
:png
|
113
129
|
end
|
@@ -126,7 +142,7 @@ private
|
|
126
142
|
|
127
143
|
# using xl-box would be weird, but doesn't seem to contradict specification
|
128
144
|
skip = ir[12, 4] == "\0\0\0\1" ? 16 : 8
|
129
|
-
case ir[
|
145
|
+
case ir[skip + 12, 4]
|
130
146
|
when 'jp2 ' then :jp2
|
131
147
|
when 'jpx ' then :jpx
|
132
148
|
end
|
@@ -137,17 +153,13 @@ private
|
|
137
153
|
end
|
138
154
|
|
139
155
|
def size_of_mng(ir)
|
140
|
-
unless ir[12, 4] == 'MHDR'
|
141
|
-
raise FormatError, 'MHDR not in place for MNG'
|
142
|
-
end
|
156
|
+
raise FormatError, 'MHDR not in place for MNG' unless ir[12, 4] == 'MHDR'
|
143
157
|
|
144
158
|
ir.unpack(16, 8, 'NN')
|
145
159
|
end
|
146
160
|
|
147
161
|
def size_of_png(ir)
|
148
|
-
unless ir[12, 4] == 'IHDR'
|
149
|
-
raise FormatError, 'IHDR not in place for PNG'
|
150
|
-
end
|
162
|
+
raise FormatError, 'IHDR not in place for PNG' unless ir[12, 4] == 'IHDR'
|
151
163
|
|
152
164
|
ir.unpack(16, 8, 'NN')
|
153
165
|
end
|
@@ -165,14 +177,12 @@ private
|
|
165
177
|
loop do
|
166
178
|
offset += 1 until [nil, section_marker].include? ir[offset, 1]
|
167
179
|
offset += 1 until section_marker != ir[offset + 1, 1]
|
168
|
-
raise FormatError, 'EOF in JPEG'
|
180
|
+
raise FormatError, 'EOF in JPEG' unless ir[offset, 1]
|
169
181
|
|
170
182
|
code, length = ir.unpack(offset, 4, 'xCn')
|
171
183
|
offset += 4
|
172
184
|
|
173
|
-
if JPEG_CODE_CHECK.include?(code)
|
174
|
-
return ir.unpack(offset + 1, 4, 'nn').reverse
|
175
|
-
end
|
185
|
+
return ir.unpack(offset + 1, 4, 'nn').reverse if JPEG_CODE_CHECK.include?(code)
|
176
186
|
|
177
187
|
offset += length - 2
|
178
188
|
end
|
@@ -237,9 +247,7 @@ private
|
|
237
247
|
def size_of_xpm(ir)
|
238
248
|
length = 1024
|
239
249
|
until (data = ir[0, length]) =~ /"\s*(\d+)\s+(\d+)(\s+\d+\s+\d+){1,2}\s*"/m
|
240
|
-
if data.length != length
|
241
|
-
raise FormatError, 'XPM size not found'
|
242
|
-
end
|
250
|
+
raise FormatError, 'XPM size not found' if data.length != length
|
243
251
|
|
244
252
|
length += 1024
|
245
253
|
end
|
@@ -268,7 +276,7 @@ private
|
|
268
276
|
tag, type = ifd.unpack(endian2b * 2)
|
269
277
|
offset += 12
|
270
278
|
|
271
|
-
next
|
279
|
+
next unless packspec[type]
|
272
280
|
|
273
281
|
value = ifd[8, 4].unpack(packspec[type])[0]
|
274
282
|
case tag
|
@@ -288,7 +296,7 @@ private
|
|
288
296
|
|
289
297
|
def size_of_swf(ir)
|
290
298
|
value_bit_length = ir.unpack1(8, 1, 'B5').to_i(2)
|
291
|
-
bit_length =
|
299
|
+
bit_length = (value_bit_length * 4) + 5
|
292
300
|
rect_bits = ir.unpack1(8, (bit_length / 8) + 1, "B#{bit_length}")
|
293
301
|
values = rect_bits[5..-1].unpack("a#{value_bit_length}" * 4).map{ |bits| bits.to_i(2) }
|
294
302
|
x_min, x_max, y_min, y_max = values
|
@@ -336,38 +344,13 @@ private
|
|
336
344
|
end
|
337
345
|
end
|
338
346
|
|
347
|
+
JP2_WALKER = ImageSize::ISOBMFF.new(
|
348
|
+
recurse: %w[jp2h],
|
349
|
+
last: %w[jp2h]
|
350
|
+
)
|
339
351
|
def size_of_jp2(ir)
|
340
|
-
|
341
|
-
|
342
|
-
in_header = false
|
343
|
-
loop do
|
344
|
-
break if stop && offset >= stop
|
345
|
-
break if ir[offset, 4] == '' || ir[offset, 4].nil?
|
346
|
-
|
347
|
-
size = ir.unpack1(offset, 4, 'N')
|
348
|
-
type = ir.fetch(offset + 4, 4)
|
349
|
-
|
350
|
-
data_offset = 8
|
351
|
-
case size
|
352
|
-
when 1
|
353
|
-
size = ir.unpack1(offset, 8, 'Q>')
|
354
|
-
data_offset = 16
|
355
|
-
raise FormatError, "Unexpected xl-box size #{size}" if (1..15).include?(size)
|
356
|
-
when 2..7
|
357
|
-
raise FormatError, "Reserved box size #{size}"
|
358
|
-
end
|
359
|
-
|
360
|
-
if type == 'jp2h'
|
361
|
-
stop = offset + size unless size.zero?
|
362
|
-
offset += data_offset
|
363
|
-
in_header = true
|
364
|
-
elsif in_header && type == 'ihdr'
|
365
|
-
return ir.unpack(offset + data_offset, 8, 'NN').reverse
|
366
|
-
else
|
367
|
-
break if size.zero? # box to the end of file
|
368
|
-
|
369
|
-
offset += size
|
370
|
-
end
|
352
|
+
JP2_WALKER.recurse(ir) do |box|
|
353
|
+
return ir.unpack(box.data_offset, 8, 'NN').reverse if box.type == 'ihdr'
|
371
354
|
end
|
372
355
|
end
|
373
356
|
alias_method :size_of_jpx, :size_of_jp2
|
@@ -391,4 +374,66 @@ private
|
|
391
374
|
(n.to_f * dpi / 2540).round
|
392
375
|
end
|
393
376
|
end
|
377
|
+
|
378
|
+
HEIF_WALKER = ImageSize::ISOBMFF.new(
|
379
|
+
recurse: %w[meta iprp ipco],
|
380
|
+
full: %w[meta hdlr pitm ipma ispe],
|
381
|
+
last: %w[meta]
|
382
|
+
)
|
383
|
+
def size_of_heif(ir)
|
384
|
+
pitm = nil
|
385
|
+
ipma = nil
|
386
|
+
ispes = {}
|
387
|
+
claps = {}
|
388
|
+
irots = {}
|
389
|
+
|
390
|
+
HEIF_WALKER.recurse(ir) do |box, _path|
|
391
|
+
case box.type
|
392
|
+
when 'hdlr'
|
393
|
+
raise FormatError, "hdlr box too small (#{box.data_size})" if box.data_size < 8
|
394
|
+
|
395
|
+
return nil unless ir[box.data_offset + 4, 4] == 'pict'
|
396
|
+
when 'pitm'
|
397
|
+
raise FormatError, 'second pitm box encountered' if pitm
|
398
|
+
|
399
|
+
pitm = box.version == 0 ? ir.unpack1(box.data_offset, 2, 'n') : ir.unpack1(box.data_offset, 4, 'N')
|
400
|
+
when 'ipma'
|
401
|
+
stream = ir.stream(box.data_offset)
|
402
|
+
|
403
|
+
property_index_16b = (box.flags & 1) == 1
|
404
|
+
|
405
|
+
ipma ||= {}
|
406
|
+
stream.unpack1(4, 'N').times do
|
407
|
+
item_id = box.version == 0 ? stream.unpack1(2, 'n') : stream.unpack1(4, 'N')
|
408
|
+
ipma[item_id] ||= Array.new(stream.unpack1(1, 'C')) do
|
409
|
+
property_index_16b ? stream.unpack1(2, 'n') & 0x7fff : stream.unpack1(1, 'C') & 0x7f
|
410
|
+
end
|
411
|
+
end
|
412
|
+
when 'ispe'
|
413
|
+
ispes[box.index] ||= ir.unpack(box.data_offset, 8, 'NN')
|
414
|
+
when 'clap'
|
415
|
+
width_n, width_d, height_n, height_d = ir.unpack(box.data_offset, 16, 'N4')
|
416
|
+
claps[box.index] ||= [Rational(width_n, width_d).round, Rational(height_n, height_d).round]
|
417
|
+
when 'irot'
|
418
|
+
irots[box.index] ||= ir.unpack1(box.data_offset, 1, 'C') & 0b11
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
return unless ipma
|
423
|
+
|
424
|
+
properties = ipma[pitm || ipma.keys.min]
|
425
|
+
return unless properties
|
426
|
+
|
427
|
+
dimensions = claps.values_at(*properties).compact.first || ispes.values_at(*properties).compact.first
|
428
|
+
return unless dimensions
|
429
|
+
|
430
|
+
irot = irots.values_at(*properties).compact.first
|
431
|
+
if irot && irot.odd?
|
432
|
+
dimensions.reverse
|
433
|
+
else
|
434
|
+
dimensions
|
435
|
+
end
|
436
|
+
end
|
437
|
+
alias_method :size_of_avif, :size_of_heif
|
438
|
+
alias_method :size_of_heic, :size_of_heif
|
394
439
|
end
|
@@ -29,7 +29,7 @@ describe ImageSize::ChunkyReader do
|
|
29
29
|
{
|
30
30
|
'empty string' => '',
|
31
31
|
'a bit of data' => 'foo bar baz',
|
32
|
-
'a lot of data' => File.
|
32
|
+
'a lot of data' => File.binread('GPL'),
|
33
33
|
}.each do |data_description, data|
|
34
34
|
{
|
35
35
|
'default' => test_reader.new(data),
|
@@ -0,0 +1,352 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rspec'
|
4
|
+
require 'stringio'
|
5
|
+
|
6
|
+
require 'image_size/string_reader'
|
7
|
+
require 'image_size/isobmff'
|
8
|
+
|
9
|
+
describe ImageSize::ISOBMFF do
|
10
|
+
boxes = Class.new do
|
11
|
+
def self.build(&block)
|
12
|
+
builder = new
|
13
|
+
builder.instance_eval(&block)
|
14
|
+
builder.string
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@io = StringIO.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def box(type, size, size64 = nil)
|
22
|
+
@io << [size, type, size64].pack("Na4#{'Q>' if size64}")
|
23
|
+
yield if block_given?
|
24
|
+
end
|
25
|
+
|
26
|
+
def data(content)
|
27
|
+
@io << content
|
28
|
+
end
|
29
|
+
|
30
|
+
def string
|
31
|
+
@io.string
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
let(:instance){ described_class.new(options) }
|
36
|
+
|
37
|
+
let(:string_reader){ ImageSize::StringReader.new(data) }
|
38
|
+
|
39
|
+
describe '#walk' do
|
40
|
+
let(:options){ {} }
|
41
|
+
|
42
|
+
def is_expected
|
43
|
+
expect{ |b| instance.walk(string_reader, &b) }
|
44
|
+
end
|
45
|
+
|
46
|
+
describe 'for multiple boxes' do
|
47
|
+
let(:data) do
|
48
|
+
boxes.build do
|
49
|
+
box('abcd', 8 + 42){ data 'x' * 42 }
|
50
|
+
box('efgh', 8 + 10)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
it do
|
55
|
+
is_expected.to yield_successive_args(
|
56
|
+
having_attributes(type: 'abcd', data_offset: 8, data_size: 42),
|
57
|
+
having_attributes(type: 'efgh', data_offset: 58, data_size: 10)
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe 'for empty data' do
|
63
|
+
let(:data){ '' }
|
64
|
+
|
65
|
+
it{ is_expected.not_to yield_control }
|
66
|
+
end
|
67
|
+
|
68
|
+
describe 'for not enough data' do
|
69
|
+
let(:data){ 'test' }
|
70
|
+
|
71
|
+
it{ is_expected.to raise_error ImageSize::FormatError }
|
72
|
+
end
|
73
|
+
|
74
|
+
describe 'for a box without content' do
|
75
|
+
let(:data){ boxes.build{ box('test', 8) } }
|
76
|
+
|
77
|
+
it{ is_expected.to yield_successive_args(having_attributes(type: 'test', data_offset: 8, data_size: 0)) }
|
78
|
+
end
|
79
|
+
|
80
|
+
describe 'for a box with content' do
|
81
|
+
let(:data){ boxes.build{ box('test', 8 + 42) } }
|
82
|
+
|
83
|
+
it do
|
84
|
+
is_expected.to yield_successive_args(having_attributes(type: 'test', data_offset: 8, data_size: 42))
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe 'for not enough data in second box' do
|
89
|
+
let(:data) do
|
90
|
+
boxes.build do
|
91
|
+
box('test', 8)
|
92
|
+
data 'test'
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
it{ is_expected.to yield_control.and raise_error ImageSize::FormatError }
|
97
|
+
end
|
98
|
+
|
99
|
+
describe 'for size-less box' do
|
100
|
+
let(:data){ boxes.build{ box('test', 0) } }
|
101
|
+
|
102
|
+
it do
|
103
|
+
is_expected.to yield_successive_args(having_attributes(type: 'test', data_offset: 8, data_size: nil))
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
(2..7).each do |size|
|
108
|
+
describe "for wrong small box size #{size}" do
|
109
|
+
let(:data){ boxes.build{ box('test', size) } }
|
110
|
+
|
111
|
+
it{ is_expected.to raise_error ImageSize::FormatError }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe 'for a big box without content' do
|
116
|
+
let(:data){ boxes.build{ box('test', 1, 16) } }
|
117
|
+
|
118
|
+
it do
|
119
|
+
is_expected.to yield_successive_args(having_attributes(type: 'test', data_offset: 16, data_size: 0))
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
describe 'for a big box with content' do
|
124
|
+
let(:data){ boxes.build{ box('test', 1, 16 + 42) } }
|
125
|
+
|
126
|
+
it do
|
127
|
+
is_expected.to yield_successive_args(having_attributes(type: 'test', data_offset: 16, data_size: 42))
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
describe 'for a full box' do
|
132
|
+
let(:options){ { full: %w[test] } }
|
133
|
+
|
134
|
+
let(:data) do
|
135
|
+
boxes.build do
|
136
|
+
box('test', 8 + 42)
|
137
|
+
data 'abcd'
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
it do
|
142
|
+
is_expected.to yield_successive_args(
|
143
|
+
having_attributes(
|
144
|
+
type: 'test',
|
145
|
+
data_offset: 12,
|
146
|
+
data_size: 38,
|
147
|
+
version: 0x61,
|
148
|
+
flags: 0x626364
|
149
|
+
)
|
150
|
+
)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
describe 'for a big full box' do
|
155
|
+
let(:options){ { full: %w[test] } }
|
156
|
+
|
157
|
+
let(:data) do
|
158
|
+
boxes.build do
|
159
|
+
box('test', 1, 16 + 42)
|
160
|
+
data 'abcd'
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
it do
|
165
|
+
is_expected.to yield_successive_args(
|
166
|
+
having_attributes(
|
167
|
+
type: 'test',
|
168
|
+
data_offset: 20,
|
169
|
+
data_size: 38,
|
170
|
+
version: 0x61,
|
171
|
+
flags: 0x626364
|
172
|
+
)
|
173
|
+
)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
16.times do |size|
|
178
|
+
describe "for wrong big box size #{size}" do
|
179
|
+
let(:data){ boxes.build{ box('test', 1, size) } }
|
180
|
+
|
181
|
+
it{ is_expected.to raise_error ImageSize::FormatError }
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
context 'given offset' do
|
186
|
+
let(:data) do
|
187
|
+
boxes.build do
|
188
|
+
box('test', 8)
|
189
|
+
box('fooo', 8)
|
190
|
+
box('barr', 8)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def is_expected
|
195
|
+
expect{ |b| instance.walk(string_reader, offset, &b) }
|
196
|
+
end
|
197
|
+
|
198
|
+
describe 'for offset at the end' do
|
199
|
+
let(:offset){ 24 }
|
200
|
+
|
201
|
+
it{ is_expected.not_to yield_control }
|
202
|
+
end
|
203
|
+
|
204
|
+
describe 'for offset at second box' do
|
205
|
+
let(:offset){ 8 }
|
206
|
+
|
207
|
+
it do
|
208
|
+
is_expected.to yield_successive_args(
|
209
|
+
having_attributes(type: 'fooo', data_offset: 16, data_size: 0),
|
210
|
+
having_attributes(type: 'barr', data_offset: 24, data_size: 0)
|
211
|
+
)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
context 'given offset and length' do
|
217
|
+
def is_expected
|
218
|
+
expect{ |b| instance.walk(string_reader, offset, length, &b) }
|
219
|
+
end
|
220
|
+
|
221
|
+
describe 'for offset at second box' do
|
222
|
+
let(:data) do
|
223
|
+
boxes.build do
|
224
|
+
box('test', 8)
|
225
|
+
box('fooo', 8)
|
226
|
+
box('barr', 8)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
let(:offset){ 8 }
|
230
|
+
let(:length){ 8 }
|
231
|
+
|
232
|
+
it do
|
233
|
+
is_expected.to yield_successive_args(having_attributes(type: 'fooo', data_offset: 16, data_size: 0))
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
describe '#recurse' do
|
240
|
+
let(:data) do
|
241
|
+
boxes.build do
|
242
|
+
box('fooA', 8 + 8 + 2) do
|
243
|
+
box('fooB', 8 + 2){ data '11' }
|
244
|
+
end
|
245
|
+
box('barA', 8 + 8 + 8 + 2) do
|
246
|
+
box('barB', 8 + 8 + 2) do
|
247
|
+
box('barC', 8 + 2) do
|
248
|
+
data '22'
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
box('bazA', 8 + 8 + 2) do
|
253
|
+
box('bazB', 8 + 2){ data '33' }
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
context 'when configured to recures all' do
|
259
|
+
let(:options){ { recurse: %w[fooA barA barB bazA] } }
|
260
|
+
|
261
|
+
it 'recurses complete tree' do
|
262
|
+
enum = instance.to_enum(:recurse, string_reader)
|
263
|
+
|
264
|
+
expect(enum.next).to have_attributes(type: 'fooA', data_offset: 8, data_size: 10)
|
265
|
+
|
266
|
+
expect(enum.next).to have_attributes(type: 'fooB', data_offset: 16, data_size: 2)
|
267
|
+
|
268
|
+
expect(enum.next).to have_attributes(type: 'barA', data_offset: 26, data_size: 18)
|
269
|
+
|
270
|
+
expect(enum.next).to have_attributes(type: 'barB', data_offset: 34, data_size: 10)
|
271
|
+
|
272
|
+
expect(enum.next).to have_attributes(type: 'barC', data_offset: 42, data_size: 2)
|
273
|
+
|
274
|
+
expect(enum.next).to have_attributes(type: 'bazA', data_offset: 52, data_size: 10)
|
275
|
+
|
276
|
+
expect(enum.next).to have_attributes(type: 'bazB', data_offset: 60, data_size: 2)
|
277
|
+
|
278
|
+
expect{ enum.next }.to raise_exception(StopIteration)
|
279
|
+
end
|
280
|
+
|
281
|
+
it 'returns nil' do
|
282
|
+
expect(instance.recurse(string_reader){ :foo }).to be_nil
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
context 'when configured to recurse part' do
|
287
|
+
let(:options){ { recurse: %w[barA] } }
|
288
|
+
|
289
|
+
it 'recurses requested part' do
|
290
|
+
enum = instance.to_enum(:recurse, string_reader)
|
291
|
+
|
292
|
+
expect(enum.next).to have_attributes(type: 'fooA', data_offset: 8, data_size: 10)
|
293
|
+
|
294
|
+
expect(enum.next).to have_attributes(type: 'barA', data_offset: 26, data_size: 18)
|
295
|
+
|
296
|
+
expect(enum.next).to have_attributes(type: 'barB', data_offset: 34, data_size: 10)
|
297
|
+
|
298
|
+
expect(enum.next).to have_attributes(type: 'bazA', data_offset: 52, data_size: 10)
|
299
|
+
|
300
|
+
expect{ enum.next }.to raise_exception(StopIteration)
|
301
|
+
end
|
302
|
+
|
303
|
+
it 'returns nil' do
|
304
|
+
expect(instance.recurse(string_reader){ :foo }).to be_nil
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
context 'when configured to not recurse' do
|
309
|
+
let(:options){ {} }
|
310
|
+
|
311
|
+
it 'does not recurse' do
|
312
|
+
enum = instance.to_enum(:recurse, string_reader)
|
313
|
+
|
314
|
+
expect(enum.next).to have_attributes(type: 'fooA', data_offset: 8, data_size: 10)
|
315
|
+
|
316
|
+
expect(enum.next).to have_attributes(type: 'barA', data_offset: 26, data_size: 18)
|
317
|
+
|
318
|
+
expect(enum.next).to have_attributes(type: 'bazA', data_offset: 52, data_size: 10)
|
319
|
+
|
320
|
+
expect{ enum.next }.to raise_exception(StopIteration)
|
321
|
+
end
|
322
|
+
|
323
|
+
it 'returns nil' do
|
324
|
+
expect(instance.recurse(string_reader){ :foo }).to be_nil
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
context 'when configured to stop' do
|
329
|
+
let(:options){ { recurse: %w[fooA barA barB bazA], last: %w[barA] } }
|
330
|
+
|
331
|
+
it 'recurses complete tree' do
|
332
|
+
enum = instance.to_enum(:recurse, string_reader)
|
333
|
+
|
334
|
+
expect(enum.next).to have_attributes(type: 'fooA', data_offset: 8, data_size: 10)
|
335
|
+
|
336
|
+
expect(enum.next).to have_attributes(type: 'fooB', data_offset: 16, data_size: 2)
|
337
|
+
|
338
|
+
expect(enum.next).to have_attributes(type: 'barA', data_offset: 26, data_size: 18)
|
339
|
+
|
340
|
+
expect(enum.next).to have_attributes(type: 'barB', data_offset: 34, data_size: 10)
|
341
|
+
|
342
|
+
expect(enum.next).to have_attributes(type: 'barC', data_offset: 42, data_size: 2)
|
343
|
+
|
344
|
+
expect{ enum.next }.to raise_exception(StopIteration)
|
345
|
+
end
|
346
|
+
|
347
|
+
it 'returns nil' do
|
348
|
+
expect(instance.recurse(string_reader){ :foo }).to be_nil
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
data/spec/image_size_spec.rb
CHANGED
@@ -10,6 +10,10 @@ require 'shellwords'
|
|
10
10
|
|
11
11
|
require 'test_server'
|
12
12
|
|
13
|
+
RSpec.configure do |config|
|
14
|
+
config.order = :random
|
15
|
+
end
|
16
|
+
|
13
17
|
describe ImageSize do
|
14
18
|
before :all do
|
15
19
|
@server = TestServer.new
|
@@ -19,6 +23,19 @@ describe ImageSize do
|
|
19
23
|
@server.finish
|
20
24
|
end
|
21
25
|
|
26
|
+
def retry_on(exception_class)
|
27
|
+
attempt = 1
|
28
|
+
begin
|
29
|
+
yield
|
30
|
+
rescue exception_class => e
|
31
|
+
warn "Attempt #{attempt}: #{e.inspect}"
|
32
|
+
raise unless attempt < 3
|
33
|
+
|
34
|
+
attempt += 1
|
35
|
+
retry
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
22
39
|
def supported_formats
|
23
40
|
ImageSize.private_instance_methods.map{ |name| name[/\Asize_of_(.*)\z/, 1] }.compact.sort
|
24
41
|
end
|
@@ -45,19 +62,26 @@ describe ImageSize do
|
|
45
62
|
describe "for #{path}" do
|
46
63
|
let(:name){ File.basename(path) }
|
47
64
|
let(:attributes) do
|
48
|
-
match = /(\d+)x(\d+)\.([^.]+)$/.match(name)
|
49
|
-
|
65
|
+
if (match = /(\d+)x(\d+)\.([^.]+)$/.match(name))
|
66
|
+
width = match[1].to_i
|
67
|
+
height = match[2].to_i
|
68
|
+
format = match[3].to_sym
|
69
|
+
end
|
50
70
|
size = format && [width, height]
|
71
|
+
media_types = ImageSize::MEDIA_TYPES[format] || []
|
72
|
+
media_type = format && media_types.first.to_s
|
51
73
|
{
|
52
|
-
:
|
53
|
-
:
|
54
|
-
:
|
55
|
-
:
|
56
|
-
:
|
57
|
-
:
|
74
|
+
format: format,
|
75
|
+
width: width,
|
76
|
+
height: height,
|
77
|
+
w: width,
|
78
|
+
h: height,
|
79
|
+
size: size,
|
80
|
+
media_type: media_type,
|
81
|
+
media_types: media_types,
|
58
82
|
}
|
59
83
|
end
|
60
|
-
let(:file_data){ File.
|
84
|
+
let(:file_data){ File.binread(path) }
|
61
85
|
let(:file_size){ file_data.length }
|
62
86
|
|
63
87
|
before do
|
@@ -148,14 +172,18 @@ describe ImageSize do
|
|
148
172
|
context 'supporting range' do
|
149
173
|
context 'without redirects' do
|
150
174
|
it 'gets format and dimensions' do
|
151
|
-
image_size =
|
175
|
+
image_size = retry_on Timeout::Error do
|
176
|
+
ImageSize.url(file_url)
|
177
|
+
end
|
152
178
|
expect(image_size).to have_attributes(attributes)
|
153
179
|
end
|
154
180
|
end
|
155
181
|
|
156
182
|
context 'with redirects' do
|
157
183
|
it 'gets format and dimensions' do
|
158
|
-
image_size =
|
184
|
+
image_size = retry_on Timeout::Error do
|
185
|
+
ImageSize.url("#{file_url}?redirect=5")
|
186
|
+
end
|
159
187
|
expect(image_size).to have_attributes(attributes)
|
160
188
|
end
|
161
189
|
end
|
@@ -163,7 +191,9 @@ describe ImageSize do
|
|
163
191
|
context 'with too many redirects' do
|
164
192
|
it 'gets format and dimensions' do
|
165
193
|
expect do
|
166
|
-
|
194
|
+
retry_on Timeout::Error do
|
195
|
+
ImageSize.url("#{file_url}?redirect=6")
|
196
|
+
end
|
167
197
|
end.to raise_error(/Too many redirects/)
|
168
198
|
end
|
169
199
|
end
|
@@ -172,14 +202,18 @@ describe ImageSize do
|
|
172
202
|
context 'not supporting range' do
|
173
203
|
context 'without redirects' do
|
174
204
|
it 'gets format and dimensions' do
|
175
|
-
image_size =
|
205
|
+
image_size = retry_on Timeout::Error do
|
206
|
+
ImageSize.url("#{file_url}?ignore_range")
|
207
|
+
end
|
176
208
|
expect(image_size).to have_attributes(attributes)
|
177
209
|
end
|
178
210
|
end
|
179
211
|
|
180
212
|
context 'with redirects' do
|
181
213
|
it 'gets format and dimensions' do
|
182
|
-
image_size =
|
214
|
+
image_size = retry_on Timeout::Error do
|
215
|
+
ImageSize.url("#{file_url}?ignore_range&redirect=5")
|
216
|
+
end
|
183
217
|
expect(image_size).to have_attributes(attributes)
|
184
218
|
end
|
185
219
|
end
|
@@ -187,7 +221,9 @@ describe ImageSize do
|
|
187
221
|
context 'with too many redirects' do
|
188
222
|
it 'gets format and dimensions' do
|
189
223
|
expect do
|
190
|
-
|
224
|
+
retry_on Timeout::Error do
|
225
|
+
ImageSize.url("#{file_url}?ignore_range&redirect=6")
|
226
|
+
end
|
191
227
|
end.to raise_error(/Too many redirects/)
|
192
228
|
end
|
193
229
|
end
|
@@ -203,8 +239,8 @@ describe ImageSize do
|
|
203
239
|
end
|
204
240
|
|
205
241
|
{
|
206
|
-
:
|
207
|
-
:
|
242
|
+
png: "\211PNG\r\n\032\n",
|
243
|
+
jpeg: "\377\330",
|
208
244
|
}.each do |type, data|
|
209
245
|
it "raises FormatError if invalid #{type} given" do
|
210
246
|
expect do
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/spec/test_server.rb
CHANGED
@@ -1,18 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'webrick'
|
4
|
+
require 'stringio'
|
4
5
|
|
5
6
|
class TestServer
|
6
7
|
attr_reader :base_url
|
7
8
|
|
8
9
|
def initialize(host = '127.0.0.1')
|
9
10
|
server_options = {
|
10
|
-
:
|
11
|
-
:
|
12
|
-
:
|
13
|
-
:
|
14
|
-
:
|
15
|
-
:
|
11
|
+
Logger: WEBrick::Log.new(StringIO.new),
|
12
|
+
AccessLog: [],
|
13
|
+
BindAddress: host,
|
14
|
+
Port: 0, # get the next available port
|
15
|
+
DocumentRoot: '.',
|
16
|
+
RequestCallback: proc do |req, res|
|
16
17
|
redirect = req.query['redirect'].to_i
|
17
18
|
if redirect > 0
|
18
19
|
res.set_redirect(
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: image_size
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Keisuke Minami
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2024-01-16 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
@@ -53,9 +53,9 @@ dependencies:
|
|
53
53
|
- - "~>"
|
54
54
|
- !ruby/object:Gem::Version
|
55
55
|
version: '2.0'
|
56
|
-
description: 'Measure following file dimensions: apng, bmp, cur, emf, gif,
|
57
|
-
jp2, jpeg, jpx, mng, pam, pbm, pcx, pgm, png, ppm, psd, svg, swf,
|
58
|
-
xpm'
|
56
|
+
description: 'Measure following file dimensions: apng, avif, bmp, cur, emf, gif, heic,
|
57
|
+
heif, ico, j2c, jp2, jpeg, jpx, mng, pam, pbm, pcx, pgm, png, ppm, psd, svg, swf,
|
58
|
+
tiff, webp, xbm, xpm'
|
59
59
|
email:
|
60
60
|
executables: []
|
61
61
|
extensions: []
|
@@ -74,12 +74,17 @@ files:
|
|
74
74
|
- image_size.gemspec
|
75
75
|
- lib/image_size.rb
|
76
76
|
- lib/image_size/chunky_reader.rb
|
77
|
+
- lib/image_size/format_error.rb
|
78
|
+
- lib/image_size/isobmff.rb
|
79
|
+
- lib/image_size/media_types.rb
|
77
80
|
- lib/image_size/reader.rb
|
78
81
|
- lib/image_size/seekable_io_reader.rb
|
79
82
|
- lib/image_size/stream_io_reader.rb
|
80
83
|
- lib/image_size/string_reader.rb
|
84
|
+
- lib/image_size/uri.rb
|
81
85
|
- lib/image_size/uri_reader.rb
|
82
86
|
- spec/image_size/chunky_reader_spec.rb
|
87
|
+
- spec/image_size/isobmff_spec.rb
|
83
88
|
- spec/image_size/seekable_io_reader_spec.rb
|
84
89
|
- spec/image_size_spec.rb
|
85
90
|
- spec/images/.gitattributes
|
@@ -91,6 +96,11 @@ files:
|
|
91
96
|
- spec/images/emf/77x77.emf
|
92
97
|
- spec/images/empty
|
93
98
|
- spec/images/gif/668x481.gif
|
99
|
+
- spec/images/heif/ap_maloletka_mariupol.452x301.heic
|
100
|
+
- spec/images/heif/maxar_mariupol.400x300.avif
|
101
|
+
- spec/images/heif/multiple.169x83.heic
|
102
|
+
- spec/images/heif/rotate.73x173.avif
|
103
|
+
- spec/images/heif/sequence.7x5.avif
|
94
104
|
- spec/images/ico/32x256.ico
|
95
105
|
- spec/images/jp2/163x402.jp2
|
96
106
|
- spec/images/jp2/176x373.jpx
|
@@ -130,7 +140,7 @@ licenses:
|
|
130
140
|
metadata:
|
131
141
|
bug_tracker_uri: https://github.com/toy/image_size/issues
|
132
142
|
changelog_uri: https://github.com/toy/image_size/blob/master/CHANGELOG.markdown
|
133
|
-
documentation_uri: https://www.rubydoc.info/gems/image_size/3.
|
143
|
+
documentation_uri: https://www.rubydoc.info/gems/image_size/3.4.0
|
134
144
|
source_code_uri: https://github.com/toy/image_size
|
135
145
|
post_install_message:
|
136
146
|
rdoc_options: []
|
@@ -140,19 +150,20 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
140
150
|
requirements:
|
141
151
|
- - ">="
|
142
152
|
- !ruby/object:Gem::Version
|
143
|
-
version:
|
153
|
+
version: 1.9.3
|
144
154
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
145
155
|
requirements:
|
146
156
|
- - ">="
|
147
157
|
- !ruby/object:Gem::Version
|
148
158
|
version: '0'
|
149
159
|
requirements: []
|
150
|
-
rubygems_version: 3.
|
160
|
+
rubygems_version: 3.4.20
|
151
161
|
signing_key:
|
152
162
|
specification_version: 4
|
153
|
-
summary: Measure image size using pure Ruby
|
163
|
+
summary: Measure image size/dimensions using pure Ruby
|
154
164
|
test_files:
|
155
165
|
- spec/image_size/chunky_reader_spec.rb
|
166
|
+
- spec/image_size/isobmff_spec.rb
|
156
167
|
- spec/image_size/seekable_io_reader_spec.rb
|
157
168
|
- spec/image_size_spec.rb
|
158
169
|
- spec/images/.gitattributes
|
@@ -164,6 +175,11 @@ test_files:
|
|
164
175
|
- spec/images/emf/77x77.emf
|
165
176
|
- spec/images/empty
|
166
177
|
- spec/images/gif/668x481.gif
|
178
|
+
- spec/images/heif/ap_maloletka_mariupol.452x301.heic
|
179
|
+
- spec/images/heif/maxar_mariupol.400x300.avif
|
180
|
+
- spec/images/heif/multiple.169x83.heic
|
181
|
+
- spec/images/heif/rotate.73x173.avif
|
182
|
+
- spec/images/heif/sequence.7x5.avif
|
167
183
|
- spec/images/ico/32x256.ico
|
168
184
|
- spec/images/jp2/163x402.jp2
|
169
185
|
- spec/images/jp2/176x373.jpx
|