image_size 3.4.0 → 3.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d88e4be6d69bedc0f976c9b73544a20ac08ab66d6b44ce1061d1374f9d2a9dc8
4
- data.tar.gz: 5b2986943b8bbeb339d0c31d0a98c5847a1a86141d5e754adaf0bd01e3d8d60f
3
+ metadata.gz: 2a128281e6674bd684b8485151f3938a5d34d8fdf146e293e908e8cfb5c762d5
4
+ data.tar.gz: 0615d7750931da9cf45ec6003fd1f20dba7638e393a6d18bbec53901c078e864
5
5
  SHA512:
6
- metadata.gz: 1b75df7c37cbcca0886eade3e47b082cde0468331c954d0605130cba564bb7718d777628599fea7332e2ae1ef9f15b80ce77072352a109dadbf9749fea049572
7
- data.tar.gz: ffc57b47280776fc7be3cb2866699f32f90a2e036b2dca703ac4262aa77750032ffb54700437958ba7b4948c934d5ea6e41f2acad4e7e71ddec84a98f18edabf
6
+ metadata.gz: d2d3cf28085954077a3016735c15ede7827dd1a72afd0617182d4278f0890f8ef41cbc8e84fe7d2661a270a8ce570b6e3cfeaa148c8be4d141c3a233cb712a59
7
+ data.tar.gz: a6dd7107a1c344a4963cf527d80e76c08e6b64cf22ae2edb6d2c7dad022f3452505a369fc9a4b4c665136e34ee2379a41b235e5d89881a32721c49ae14c6b6aa
@@ -0,0 +1,8 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: /
5
+ schedule:
6
+ interval: daily
7
+ cooldown:
8
+ default-days: 7
@@ -6,10 +6,13 @@ on:
6
6
  - cron: 45 4 * * 3
7
7
  jobs:
8
8
  check:
9
- runs-on: ubuntu-latest
9
+ runs-on: ubuntu-22.04
10
10
  strategy:
11
11
  matrix:
12
12
  ruby:
13
+ - '1.9.3'
14
+ - '2.0'
15
+ - '2.1'
13
16
  - '2.3'
14
17
  - '2.4'
15
18
  - '2.5'
@@ -19,31 +22,18 @@ jobs:
19
22
  - '3.1'
20
23
  - '3.2'
21
24
  - '3.3'
22
- - jruby-9.3
25
+ - '3.4'
26
+ - '4.0'
23
27
  - jruby-9.4
28
+ - jruby-10.1
24
29
  fail-fast: false
25
30
  steps:
26
- - uses: actions/checkout@v3
31
+ - uses: actions/checkout@v6
27
32
  - uses: ruby/setup-ruby@v1
28
33
  with:
29
34
  ruby-version: "${{ matrix.ruby }}"
30
35
  bundler-cache: true
31
36
  - run: bundle exec rspec --format documentation
32
- legacy:
33
- runs-on: ubuntu-latest
34
- container: ${{ matrix.container }}
35
- strategy:
36
- matrix:
37
- container:
38
- - rspec/ci:1.9.3
39
- - ruby:2.0
40
- - ruby:2.1
41
- - ruby:2.2
42
- fail-fast: false
43
- steps:
44
- - uses: actions/checkout@v3
45
- - run: bundle install
46
- - run: bundle exec rspec --format documentation
47
37
  windows:
48
38
  runs-on: windows-latest
49
39
  strategy:
@@ -55,9 +45,11 @@ jobs:
55
45
  - '3.1'
56
46
  - '3.2'
57
47
  - '3.3'
48
+ - '3.4'
49
+ - '4.0'
58
50
  fail-fast: false
59
51
  steps:
60
- - uses: actions/checkout@v3
52
+ - uses: actions/checkout@v6
61
53
  - uses: ruby/setup-ruby@v1
62
54
  with:
63
55
  ruby-version: "${{ matrix.ruby }}"
@@ -8,9 +8,9 @@ jobs:
8
8
  rubocop:
9
9
  runs-on: ubuntu-latest
10
10
  steps:
11
- - uses: actions/checkout@v3
11
+ - uses: actions/checkout@v6
12
12
  - uses: ruby/setup-ruby@v1
13
13
  with:
14
- ruby-version: '3'
14
+ ruby-version: '4'
15
15
  bundler-cache: true
16
16
  - run: bundle exec rubocop
data/.rubocop.yml CHANGED
@@ -50,6 +50,9 @@ Style/NumericPredicate:
50
50
  Style/SafeNavigation:
51
51
  Enabled: false
52
52
 
53
+ Style/SignalException:
54
+ EnforcedStyle: semantic
55
+
53
56
  Style/SlicingWithRange:
54
57
  Enabled: false
55
58
 
data/.rubocop_todo.yml CHANGED
@@ -20,9 +20,3 @@ Metrics/CyclomaticComplexity:
20
20
  # Configuration parameters: IgnoredMethods.
21
21
  Metrics/PerceivedComplexity:
22
22
  Enabled: false
23
-
24
- # Offense count: 8
25
- # Cop supports --auto-correct.
26
- Style/PerlBackrefs:
27
- Exclude:
28
- - 'lib/image_size.rb'
data/CHANGELOG.markdown CHANGED
@@ -2,11 +2,20 @@
2
2
 
3
3
  ## unreleased
4
4
 
5
+ ## v3.5.0 (2026-05-03)
6
+
7
+ * Add `byte_size` method to expose the size of the image data in bytes [#27](https://github.com/toy/image_size/pull/27) [@dgodd](https://github.com/dgodd)
8
+ * Add minimal validation of `dpi` global configuration [@toy](https://github.com/toy)
9
+ * Make `chunk_size` globally configurable [@toy](https://github.com/toy)
10
+ * Make `max_redirects` globally configurable [@toy](https://github.com/toy)
11
+ * Prevent requesting chunks over http after end of file [#29](https://github.com/toy/image_size/issues/29) [@toy](https://github.com/toy)
12
+ * Add ability to restrict fetched URIs by setting `uri_checker` proc [@toy](https://github.com/toy)
13
+
5
14
  ## v3.4.0 (2024-01-16)
6
15
 
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)
16
+ * 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)
17
+ * Allow fetching from HTTP server by requiring `image_size/uri` [@toy](https://github.com/toy)
18
+ * Fix for `ArgumentError` when requiring only `image_size/uri_reader` (without image_size) [@toy](https://github.com/toy)
10
19
  * Require ruby 1.9.3 [@toy](https://github.com/toy)
11
20
 
12
21
  ## v3.3.0 (2023-05-30)
data/README.markdown CHANGED
@@ -1,5 +1,5 @@
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/actions/workflow/status/toy/image_size/check.yml?logo=github)](https://github.com/toy/image_size/actions/workflows/check.yml)
2
+ [![Check](https://img.shields.io/github/actions/workflow/status/toy/image_size/check.yml?label=check&logo=github)](https://github.com/toy/image_size/actions/workflows/check.yml)
3
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
@@ -40,6 +40,7 @@ image_size.size.w #=> 436
40
40
  image_size.size.h #=> 429
41
41
  image_size.media_type #=> "image/jpeg"
42
42
  image_size.media_types #=> ["image/jpeg"]
43
+ image_size.byte_size #=> 10938
43
44
  ```
44
45
 
45
46
  Or using `IO` object:
@@ -92,6 +93,20 @@ File.open('spec/images/jpeg/436x429.jpeg', 'rb') do |fh|
92
93
  end
93
94
  ```
94
95
 
96
+ ### Configuration
97
+
98
+ DPI used for converting `svg` and `emf` dimensions can be configured from default 72:
99
+
100
+ ```ruby
101
+ ImageSize.dpi = 150
102
+ ```
103
+
104
+ Chunk size used for reading files, IO and from remote can be configured from default 4096:
105
+
106
+ ```ruby
107
+ ImageSize.chunk_size = 256
108
+ ```
109
+
95
110
  ### Experimental: fetch image meta from HTTP server
96
111
 
97
112
  If server recognises Range header, only needed chunks will be fetched even for TIFF images, otherwise required amount
@@ -140,9 +155,41 @@ puts Benchmark.measure{ p ImageSize.url(url).size }
140
155
  0.006247 0.001045 0.007292 ( 0.197631)
141
156
  ```
142
157
 
158
+ #### Configuration
159
+
160
+ Maximum number of redirects can be configured from default 5:
161
+
162
+ ```ruby
163
+ ImageSize.max_redirects = 10
164
+ ```
165
+
166
+ Or redirects can be disabled:
167
+
168
+ ```ruby
169
+ ImageSize.max_redirects = 0
170
+ ```
171
+
172
+ A url checker can be added to reduce SSRF risk:
173
+
174
+ ```ruby
175
+ ImageSize.uri_checker = lambda do |uri|
176
+ raise 'host is not allowed' unless uri.host == 'upload.wikipedia.org'
177
+ end
178
+ ```
179
+
180
+ Alternatively `private_address_check` gem can be of help:
181
+
182
+ ```ruby
183
+ require "private_address_check/tcpsocket_ext"
184
+
185
+ PrivateAddressCheck.only_public_connections do
186
+ ImageSize.url(url).size
187
+ end
188
+ ```
189
+
143
190
  ## Licence
144
191
 
145
192
  This code is free to use under the terms of the [Ruby's licence](LICENSE.txt).
146
193
 
147
194
  Original author: Keisuke Minami <keisuke@rccn.com>.\
148
- Further development 2010-2024 Ivan Kuchin https://github.com/toy/image_size
195
+ Further development 2010-2026 Ivan Kuchin https://github.com/toy/image_size
data/image_size.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'image_size'
5
- s.version = '3.4.0'
5
+ s.version = '3.5.0'
6
6
  s.summary = %q{Measure image size/dimensions using pure Ruby}
7
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}"
@@ -16,11 +16,20 @@ Gem::Specification.new do |s|
16
16
  'changelog_uri' => "https://github.com/toy/#{s.name}/blob/master/CHANGELOG.markdown",
17
17
  'documentation_uri' => "https://www.rubydoc.info/gems/#{s.name}/#{s.version}",
18
18
  'source_code_uri' => "https://github.com/toy/#{s.name}",
19
- }
19
+ } if s.respond_to?(:metadata=)
20
20
 
21
- s.files = `git ls-files`.split("\n")
22
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
23
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ s.files = Dir[*%w[
22
+ .gitignore
23
+ .rubocop*.yml
24
+ Gemfile
25
+ LICENSE.txt
26
+ GPL
27
+ *.markdown
28
+ *.gemspec
29
+ {.github,lib,spec}/**/{*,.gitattributes}
30
+ ]].reject(&File.method(:directory?))
31
+
32
+ s.test_files = Dir['spec/**/{*,.gitattributes}'].reject(&File.method(:directory?))
24
33
  s.require_paths = %w[lib]
25
34
 
26
35
  s.add_development_dependency 'rspec', '~> 3.0'
@@ -8,7 +8,7 @@ class ImageSize
8
8
 
9
9
  # Size of a chunk in which to read
10
10
  def chunk_size
11
- 4096
11
+ @chunk_size ||= ImageSize.chunk_size
12
12
  end
13
13
 
14
14
  # Including class should define method chunk that accepts the chunk number
@@ -18,7 +18,7 @@ class ImageSize
18
18
  # substring, behaves same as str[start, length] except start can't be
19
19
  # negative.
20
20
  def [](offset, length)
21
- raise ArgumentError, "expected offset not to be negative, got #{offset}" if offset < 0
21
+ fail ArgumentError, "expected offset not to be negative, got #{offset}" if offset < 0
22
22
  return if length < 0
23
23
 
24
24
  first = offset / chunk_size
@@ -56,9 +56,9 @@ class ImageSize
56
56
  when 1
57
57
  size = reader.unpack1(offset + 8, 8, 'Q>')
58
58
  relative_data_offset += 8
59
- raise FormatError, "Unexpected ISOBMFF xl-box size #{size}" if size < 16
59
+ fail FormatError, "Unexpected ISOBMFF xl-box size #{size}" if size < 16
60
60
  when 2..7
61
- raise FormatError, "Reserved ISOBMFF box size #{size}"
61
+ fail FormatError, "Reserved ISOBMFF box size #{size}"
62
62
  end
63
63
 
64
64
  attributes = {
@@ -30,14 +30,15 @@ class ImageSize
30
30
  when input.is_a?(Pathname)
31
31
  input.open('rb'){ |f| yield for_io(f) }
32
32
  else
33
- raise ArgumentError, "expected data as String or an object responding to read and eof?, got #{input.class}"
33
+ fail ArgumentError, 'expected a String, a Pathname, a StringIO or an object responding to read and eof? ' \
34
+ "(IO), got #{input.class}"
34
35
  end
35
36
  end
36
37
 
37
38
  private
38
39
 
39
40
  def for_io(io)
40
- if io.respond_to?(:stat) && !io.stat.file?
41
+ if (io.respond_to?(:stat) && !io.stat.file?) || !io.respond_to?(:seek)
41
42
  StreamIOReader.new(io)
42
43
  else
43
44
  begin
@@ -54,7 +55,7 @@ class ImageSize
54
55
  chunk = self[offset, length]
55
56
 
56
57
  unless chunk && chunk.length == length
57
- raise FormatError, "Expected #{length} bytes at offset #{offset}, got #{chunk.inspect}"
58
+ fail FormatError, "Expected #{length} bytes at offset #{offset}, got #{chunk.inspect}"
58
59
  end
59
60
 
60
61
  chunk
@@ -12,6 +12,10 @@ class ImageSize
12
12
  @chunks = {}
13
13
  end
14
14
 
15
+ def byte_size
16
+ @io.size
17
+ end
18
+
15
19
  private
16
20
 
17
21
  def chunk(i)
@@ -11,6 +11,12 @@ class ImageSize
11
11
  @chunks = []
12
12
  end
13
13
 
14
+ def byte_size
15
+ return nil unless @io.respond_to?(:stat) && @io.stat.file?
16
+
17
+ @io.stat.size
18
+ end
19
+
14
20
  private
15
21
 
16
22
  def chunk(i)
@@ -17,5 +17,9 @@ class ImageSize
17
17
  def [](offset, length)
18
18
  @string[offset, length]
19
19
  end
20
+
21
+ def byte_size
22
+ @string.bytesize
23
+ end
20
24
  end
21
25
  end
@@ -17,17 +17,24 @@ class ImageSize
17
17
  module HTTPChunkyReader # :nodoc:
18
18
  include ChunkyReader
19
19
 
20
+ def chunk_start(i)
21
+ chunk_size * i
22
+ end
23
+
20
24
  def chunk_range_header(i)
21
- { 'Range' => "bytes=#{chunk_size * i}-#{(chunk_size * (i + 1)) - 1}" }
25
+ { 'Range' => "bytes=#{chunk_start(i)}-#{chunk_start(i + 1) - 1}" }
22
26
  end
23
27
  end
24
28
 
25
29
  class BodyReader # :nodoc:
26
30
  include ChunkyReader
27
31
 
32
+ attr_reader :byte_size
33
+
28
34
  def initialize(response)
29
35
  @body = String.new
30
36
  @body_reader = response.to_enum(:read_body)
37
+ @byte_size = response.content_length
31
38
  end
32
39
 
33
40
  def [](offset, length)
@@ -46,20 +53,32 @@ class ImageSize
46
53
  class RangeReader # :nodoc:
47
54
  include HTTPChunkyReader
48
55
 
49
- def initialize(http, request_uri, chunk0)
56
+ attr_reader :byte_size
57
+
58
+ def initialize(http, request_uri, chunk0, byte_size)
50
59
  @http = http
51
60
  @request_uri = request_uri
52
61
  @chunks = { 0 => chunk0 }
62
+ @byte_size = byte_size
63
+ @last_chunk = nil
53
64
  end
54
65
 
55
66
  def chunk(i)
67
+ return if @byte_size && chunk_start(i) >= @byte_size
68
+ return if @last_chunk && i > @last_chunk
69
+
56
70
  unless @chunks.key?(i)
57
71
  response = @http.get(@request_uri, chunk_range_header(i))
58
72
  case response
59
73
  when Net::HTTPPartialContent
60
- @chunks[i] = response.body
74
+ body = response.body
75
+ @chunks[i] = body
76
+ @last_chunk = i if body.length < chunk_size
77
+ when Net::HTTPRequestedRangeNotSatisfiable
78
+ @chunks[i] = nil
79
+ @last_chunk = i if !@last_chunk || @last_chunk > i
61
80
  else
62
- raise "Unexpected response: #{response}"
81
+ fail "Unexpected response: #{response}"
63
82
  end
64
83
  end
65
84
 
@@ -70,9 +89,11 @@ class ImageSize
70
89
  class << self
71
90
  include HTTPChunkyReader
72
91
 
73
- def open(uri, max_redirects = 5)
92
+ def open(uri)
74
93
  http = nil
75
- (max_redirects + 1).times do
94
+ (ImageSize.max_redirects + 1).times do
95
+ ImageSize.uri_checker.call(uri)
96
+
76
97
  unless http && http.address == uri.host && http.port == uri.port
77
98
  http.finish if http
78
99
 
@@ -92,17 +113,19 @@ class ImageSize
92
113
  when Net::HTTPRedirection
93
114
  uri += response['location']
94
115
  when Net::HTTPPartialContent
95
- return yield RangeReader.new(http, uri.request_uri, response.body)
116
+ m = response['content-range'].match(%r{\bbytes\s+\d+-\d+/(\d+)}i) if response['content-range']
117
+ byte_size = m[1].to_i if m
118
+ return yield RangeReader.new(http, uri.request_uri, response.body, byte_size)
96
119
  when Net::HTTPRequestedRangeNotSatisfiable
97
120
  return yield StringReader.new('')
98
121
  else
99
- raise "Unexpected response: #{response}"
122
+ fail "Unexpected response: #{response}"
100
123
  end
101
124
  end
102
125
 
103
- raise "Too many redirects: #{uri}"
126
+ fail "Too many redirects: #{uri}"
104
127
  ensure
105
- http.finish if http.started?
128
+ http.finish if http && http.started?
106
129
  end
107
130
  end
108
131
  end
@@ -124,4 +147,32 @@ class ImageSize
124
147
  def self.url(url)
125
148
  new(url.is_a?(URI) ? url : URI(url))
126
149
  end
150
+
151
+ # Maximum number of redirects
152
+ def self.max_redirects
153
+ @max_redirects || 5
154
+ end
155
+
156
+ # Set maximum number of redirects
157
+ def self.max_redirects=(max_redirects)
158
+ unless max_redirects.nil? || (max_redirects.is_a?(Integer) && max_redirects >= 0)
159
+ fail ArgumentError, "max_redirects should be 0, a positive Integer or nil, got #{max_redirects}"
160
+ end
161
+
162
+ @max_redirects = max_redirects
163
+ end
164
+
165
+ # Hook to call before making every request
166
+ def self.uri_checker
167
+ @uri_checker || proc{ |_uri| }
168
+ end
169
+
170
+ # Set hook to call before making every request
171
+ def self.uri_checker=(uri_checker)
172
+ unless uri_checker.nil? || uri_checker.respond_to?(:call)
173
+ fail ArgumentError, "uri_checker should respond to call or be nil, got #{uri_checker}"
174
+ end
175
+
176
+ @uri_checker = uri_checker
177
+ end
127
178
  end
data/lib/image_size.rb CHANGED
@@ -36,14 +36,30 @@ class ImageSize
36
36
  new(Pathname.new(path))
37
37
  end
38
38
 
39
- # Used for svg
39
+ # Used for svg and emf
40
40
  def self.dpi
41
- @dpi || 72
41
+ @dpi || 72.0
42
42
  end
43
43
 
44
- # Used for svg
44
+ # Used for svg and emf
45
45
  def self.dpi=(dpi)
46
- @dpi = dpi.to_f
46
+ fail ArgumentError, "dpi should be nil or positive, got #{dpi}" unless dpi.nil? || dpi > 0
47
+
48
+ @dpi = dpi ? dpi.to_f : nil
49
+ end
50
+
51
+ # Size of chunk to use by IO and URI readers
52
+ def self.chunk_size
53
+ @chunk_size || 4096
54
+ end
55
+
56
+ # Set size of chunk to use by IO and URI readers
57
+ def self.chunk_size=(chunk_size)
58
+ unless chunk_size.nil? || (chunk_size.is_a?(Integer) && chunk_size > 0)
59
+ fail ArgumentError, "chunk_size should be a positive Integer or nil, got #{chunk_size}"
60
+ end
61
+
62
+ @chunk_size = chunk_size
47
63
  end
48
64
 
49
65
  # Given image as any class responding to read and eof? or data as String, finds its format and dimensions
@@ -51,6 +67,7 @@ class ImageSize
51
67
  Reader.open(data) do |ir|
52
68
  @format = detect_format(ir)
53
69
  @width, @height = send("size_of_#{@format}", ir) if @format
70
+ @byte_size = ir.byte_size
54
71
  end
55
72
  end
56
73
 
@@ -65,6 +82,8 @@ class ImageSize
65
82
  attr_reader :height
66
83
  alias_method :h, :height
67
84
 
85
+ attr_reader :byte_size
86
+
68
87
  # get image width and height as an array which to_s method returns "#{width}x#{height}"
69
88
  def size
70
89
  Size.new([width, height]) if format
@@ -72,7 +91,7 @@ class ImageSize
72
91
 
73
92
  # Media type (formerly known as a MIME type)
74
93
  def media_type
75
- MEDIA_TYPES.fetch(format, []).first
94
+ media_types.first
76
95
  end
77
96
 
78
97
  # All media types:
@@ -153,13 +172,13 @@ private
153
172
  end
154
173
 
155
174
  def size_of_mng(ir)
156
- raise FormatError, 'MHDR not in place for MNG' unless ir[12, 4] == 'MHDR'
175
+ fail FormatError, 'MHDR not in place for MNG' unless ir[12, 4] == 'MHDR'
157
176
 
158
177
  ir.unpack(16, 8, 'NN')
159
178
  end
160
179
 
161
180
  def size_of_png(ir)
162
- raise FormatError, 'IHDR not in place for PNG' unless ir[12, 4] == 'IHDR'
181
+ fail FormatError, 'IHDR not in place for PNG' unless ir[12, 4] == 'IHDR'
163
182
 
164
183
  ir.unpack(16, 8, 'NN')
165
184
  end
@@ -177,7 +196,7 @@ private
177
196
  loop do
178
197
  offset += 1 until [nil, section_marker].include? ir[offset, 1]
179
198
  offset += 1 until section_marker != ir[offset + 1, 1]
180
- raise FormatError, 'EOF in JPEG' unless ir[offset, 1]
199
+ fail FormatError, 'EOF in JPEG' unless ir[offset, 1]
181
200
 
182
201
  code, length = ir.unpack(offset, 4, 'xCn')
183
202
  offset += 4
@@ -206,8 +225,7 @@ private
206
225
  def size_of_ppm(ir)
207
226
  header = ir[0, 1024]
208
227
  header.gsub!(/^\#[^\n\r]*/m, '')
209
- header =~ /^(P[1-6])\s+?(\d+)\s+?(\d+)/m
210
- [$2.to_i, $3.to_i]
228
+ header.match(/^(?:P[1-6])\s+?(\d+)\s+?(\d+)/m)[1..2].map(&:to_i)
211
229
  end
212
230
  alias_method :size_of_pbm, :size_of_ppm
213
231
  alias_method :size_of_pgm, :size_of_ppm
@@ -223,35 +241,37 @@ private
223
241
  chunk = ir[offset, 32]
224
242
  case chunk
225
243
  when /\AWIDTH (\d+)\n/
226
- width = $1.to_i
244
+ width = Regexp.last_match[1].to_i
227
245
  when /\AHEIGHT (\d+)\n/
228
- height = $1.to_i
246
+ height = Regexp.last_match[1].to_i
229
247
  when /\AENDHDR\n/
230
248
  break
231
249
  when /\A(?:DEPTH|MAXVAL) \d+\n/, /\ATUPLTYPE \S+\n/
232
250
  # ignore
233
251
  else
234
- raise FormatError, "Unexpected data in PAM header: #{chunk.inspect}"
252
+ fail FormatError, "Unexpected data in PAM header: #{chunk.inspect}"
235
253
  end
236
- offset += $&.length
254
+ offset += Regexp.last_match[0].length
237
255
  end
238
256
  end
239
257
  [width, height]
240
258
  end
241
259
 
242
260
  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]
261
+ ir[0, 1024].match(/^\#define\s*\S*\s*(\d+)\s*\n\#define\s*\S*\s*(\d+)/mi)[1..2].map(&:to_i)
245
262
  end
246
263
 
247
264
  def size_of_xpm(ir)
248
265
  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
266
+ loop do
267
+ data = ir[0, length]
268
+ m = data.match(/"\s*(\d+)\s+(\d+)(?:\s+\d+\s+\d+){1,2}\s*"/m)
269
+ return m[1..2].map(&:to_i) if m
270
+
271
+ fail FormatError, 'XPM size not found' if data.length != length
251
272
 
252
273
  length += 1024
253
274
  end
254
- [$1.to_i, $2.to_i]
255
275
  end
256
276
 
257
277
  def size_of_psd(ir)
@@ -271,7 +291,7 @@ private
271
291
  width = height = nil
272
292
  until width && height
273
293
  ifd = ir.fetch(offset, 12)
274
- raise FormatError, 'Reached end of directory entries in TIFF' if offset > num_dirent
294
+ fail FormatError, 'Reached end of directory entries in TIFF' if offset > num_dirent
275
295
 
276
296
  tag, type = ifd.unpack(endian2b * 2)
277
297
  offset += 12
@@ -390,11 +410,11 @@ private
390
410
  HEIF_WALKER.recurse(ir) do |box, _path|
391
411
  case box.type
392
412
  when 'hdlr'
393
- raise FormatError, "hdlr box too small (#{box.data_size})" if box.data_size < 8
413
+ fail FormatError, "hdlr box too small (#{box.data_size})" if box.data_size < 8
394
414
 
395
415
  return nil unless ir[box.data_offset + 4, 4] == 'pict'
396
416
  when 'pitm'
397
- raise FormatError, 'second pitm box encountered' if pitm
417
+ fail FormatError, 'second pitm box encountered' if pitm
398
418
 
399
419
  pitm = box.version == 0 ? ir.unpack1(box.data_offset, 2, 'n') : ir.unpack1(box.data_offset, 4, 'N')
400
420
  when 'ipma'
@@ -436,4 +456,6 @@ private
436
456
  end
437
457
  alias_method :size_of_avif, :size_of_heif
438
458
  alias_method :size_of_heic, :size_of_heif
459
+
460
+ private_constant :SVG_R, :XML_R, :JPEG_CODE_CHECK, :JP2_WALKER, :EMF_UMAX, :EMF_SMAX, :HEIF_WALKER
439
461
  end
@@ -6,45 +6,40 @@ require 'image_size/seekable_io_reader'
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
@@ -14,6 +14,20 @@ RSpec.configure do |config|
14
14
  config.order = :random
15
15
  end
16
16
 
17
+ class RestrictedIO
18
+ def initialize(data)
19
+ @io = StringIO.new(data)
20
+ end
21
+
22
+ def read(length = nil)
23
+ @io.read(length)
24
+ end
25
+
26
+ def eof?
27
+ @io.eof?
28
+ end
29
+ end
30
+
17
31
  describe ImageSize do
18
32
  before :all do
19
33
  @server = TestServer.new
@@ -67,7 +81,7 @@ describe ImageSize do
67
81
  height = match[2].to_i
68
82
  format = match[3].to_sym
69
83
  end
70
- size = format && [width, height]
84
+ size = [width, height] if format
71
85
  media_types = ImageSize::MEDIA_TYPES[format] || []
72
86
  media_type = format && media_types.first.to_s
73
87
  {
@@ -79,6 +93,7 @@ describe ImageSize do
79
93
  size: size,
80
94
  media_type: media_type,
81
95
  media_types: media_types,
96
+ byte_size: file_size,
82
97
  }
83
98
  end
84
99
  let(:file_data){ File.binread(path) }
@@ -88,7 +103,7 @@ describe ImageSize do
88
103
  max_file_size = 16_384
89
104
 
90
105
  if file_size > max_file_size
91
- raise "reduce resulting gem size, #{path} is too big (#{file_size} > #{max_file_size})"
106
+ fail "reduce resulting gem size, #{path} is too big (#{file_size} > #{max_file_size})"
92
107
  end
93
108
  end
94
109
 
@@ -119,6 +134,8 @@ describe ImageSize do
119
134
  end
120
135
 
121
136
  context 'given as unseekable IO' do
137
+ let(:attributes){ super().merge(byte_size: nil) }
138
+
122
139
  it 'gets format and dimensions' do
123
140
  IO.popen(%W[cat #{path}].shelljoin, 'rb') do |io|
124
141
  image_size = ImageSize.new(io)
@@ -128,6 +145,16 @@ describe ImageSize do
128
145
  end
129
146
  end
130
147
 
148
+ context 'given an object allowing only read and eof?' do
149
+ let(:attributes){ super().merge(byte_size: nil) }
150
+
151
+ it 'gets format and dimensions' do
152
+ io = RestrictedIO.new(file_data)
153
+ image_size = ImageSize.new(io)
154
+ expect(image_size).to have_attributes(attributes)
155
+ end
156
+ end
157
+
131
158
  context 'given as StringIO' do
132
159
  it 'gets format and dimensions' do
133
160
  io = StringIO.new(file_data)
@@ -167,64 +194,74 @@ describe ImageSize do
167
194
  end
168
195
 
169
196
  context 'fetching from webserver' do
170
- let(:file_url){ @server.base_url + path }
197
+ let(:url){ "#{@server.base_url}#{path}#{query}" }
198
+
199
+ before{ ImageSize.chunk_size = 64 }
200
+ after{ ImageSize.chunk_size = nil }
201
+
202
+ subject do
203
+ retry_on Timeout::Error do
204
+ ImageSize.url(url)
205
+ end
206
+ end
171
207
 
172
208
  context 'supporting range' do
173
209
  context 'without redirects' do
210
+ let(:query){ '' }
211
+
174
212
  it 'gets format and dimensions' do
175
- image_size = retry_on Timeout::Error do
176
- ImageSize.url(file_url)
177
- end
178
- expect(image_size).to have_attributes(attributes)
213
+ is_expected.to have_attributes(attributes)
179
214
  end
180
215
  end
181
216
 
182
217
  context 'with redirects' do
218
+ let(:query){ '?redirect=5' }
219
+
183
220
  it 'gets format and dimensions' do
184
- image_size = retry_on Timeout::Error do
185
- ImageSize.url("#{file_url}?redirect=5")
186
- end
187
- expect(image_size).to have_attributes(attributes)
221
+ is_expected.to have_attributes(attributes)
188
222
  end
189
223
  end
190
224
 
191
225
  context 'with too many redirects' do
226
+ let(:query){ '?redirect=6' }
227
+
228
+ it 'raises error' do
229
+ expect{ subject }.to raise_error(/Too many redirects/)
230
+ end
231
+ end
232
+
233
+ context 'with unknown file size' do
234
+ let(:query){ '?unknown_file_size' }
235
+ let(:attributes){ super().merge(byte_size: file_size.zero? ? 0 : nil) }
236
+
192
237
  it 'gets format and dimensions' do
193
- expect do
194
- retry_on Timeout::Error do
195
- ImageSize.url("#{file_url}?redirect=6")
196
- end
197
- end.to raise_error(/Too many redirects/)
238
+ is_expected.to have_attributes(attributes)
198
239
  end
199
240
  end
200
241
  end
201
242
 
202
243
  context 'not supporting range' do
244
+ let(:query){ '?ignore_range' }
245
+
203
246
  context 'without redirects' do
204
247
  it 'gets format and dimensions' do
205
- image_size = retry_on Timeout::Error do
206
- ImageSize.url("#{file_url}?ignore_range")
207
- end
208
- expect(image_size).to have_attributes(attributes)
248
+ is_expected.to have_attributes(attributes)
209
249
  end
210
250
  end
211
251
 
212
252
  context 'with redirects' do
253
+ let(:query){ '?ignore_range&redirect=5' }
254
+
213
255
  it 'gets format and dimensions' do
214
- image_size = retry_on Timeout::Error do
215
- ImageSize.url("#{file_url}?ignore_range&redirect=5")
216
- end
217
- expect(image_size).to have_attributes(attributes)
256
+ is_expected.to have_attributes(attributes)
218
257
  end
219
258
  end
220
259
 
221
260
  context 'with too many redirects' do
222
- it 'gets format and dimensions' do
223
- expect do
224
- retry_on Timeout::Error do
225
- ImageSize.url("#{file_url}?ignore_range&redirect=6")
226
- end
227
- end.to raise_error(/Too many redirects/)
261
+ let(:query){ '?ignore_range&redirect=6' }
262
+
263
+ it 'raises error' do
264
+ expect{ subject }.to raise_error(/Too many redirects/)
228
265
  end
229
266
  end
230
267
  end
@@ -248,4 +285,164 @@ describe ImageSize do
248
285
  end.to raise_error(ImageSize::FormatError)
249
286
  end
250
287
  end
288
+
289
+ describe '.dpi' do
290
+ subject{ ImageSize.dpi }
291
+
292
+ after{ ImageSize.dpi = nil }
293
+
294
+ it 'is 72 by default' do
295
+ is_expected.to eql(72.0)
296
+ end
297
+
298
+ it 'is can be set and get converted to Float' do
299
+ ImageSize.dpi = 42
300
+
301
+ is_expected.to eql(42.0)
302
+ end
303
+
304
+ it 'is can not be set to 0' do
305
+ expect{ ImageSize.dpi = 0 }.to raise_error(ArgumentError)
306
+
307
+ is_expected.to eql(72.0)
308
+ end
309
+
310
+ it 'is can not be set to a negative value' do
311
+ expect{ ImageSize.dpi = -42 }.to raise_error(ArgumentError)
312
+
313
+ is_expected.to eql(72.0)
314
+ end
315
+
316
+ it 'can be reset' do
317
+ ImageSize.dpi = 42
318
+ ImageSize.dpi = nil
319
+
320
+ is_expected.to eql(72.0)
321
+ end
322
+ end
323
+
324
+ describe '.chunk_size' do
325
+ subject{ ImageSize.chunk_size }
326
+
327
+ after{ ImageSize.chunk_size = nil }
328
+
329
+ it 'is 4096 by default' do
330
+ is_expected.to eql(4096)
331
+ end
332
+
333
+ it 'is can be set to an Integer' do
334
+ ImageSize.chunk_size = 256
335
+
336
+ is_expected.to eql(256)
337
+ end
338
+
339
+ it 'is can not be set to 0' do
340
+ expect{ ImageSize.chunk_size = 0 }.to raise_error(ArgumentError)
341
+
342
+ is_expected.to eql(4096)
343
+ end
344
+
345
+ it 'is can not be set to a negative value' do
346
+ expect{ ImageSize.chunk_size = -1 }.to raise_error(ArgumentError)
347
+
348
+ is_expected.to eql(4096)
349
+ end
350
+
351
+ it 'is can not be set to a Float' do
352
+ expect{ ImageSize.chunk_size = 3.5 }.to raise_error(ArgumentError)
353
+
354
+ is_expected.to eql(4096)
355
+ end
356
+
357
+ it 'can be reset' do
358
+ ImageSize.chunk_size = 256
359
+ ImageSize.chunk_size = nil
360
+
361
+ is_expected.to eql(4096)
362
+ end
363
+ end
364
+
365
+ describe '.max_redirects' do
366
+ subject{ ImageSize.max_redirects }
367
+
368
+ after{ ImageSize.max_redirects = nil }
369
+
370
+ it 'is 4096 by default' do
371
+ is_expected.to eql(5)
372
+ end
373
+
374
+ it 'is can be set to an Integer' do
375
+ ImageSize.max_redirects = 3
376
+
377
+ is_expected.to eql(3)
378
+ end
379
+
380
+ it 'is can be set to 0' do
381
+ ImageSize.max_redirects = 0
382
+
383
+ is_expected.to eql(0)
384
+ end
385
+
386
+ it 'is can not be set to a negative value' do
387
+ expect{ ImageSize.max_redirects = -1 }.to raise_error(ArgumentError)
388
+
389
+ is_expected.to eql(5)
390
+ end
391
+
392
+ it 'is can not be set to a Float' do
393
+ expect{ ImageSize.max_redirects = 3.5 }.to raise_error(ArgumentError)
394
+
395
+ is_expected.to eql(5)
396
+ end
397
+
398
+ it 'can be reset' do
399
+ ImageSize.max_redirects = 3
400
+ ImageSize.max_redirects = nil
401
+
402
+ is_expected.to eql(5)
403
+ end
404
+ end
405
+
406
+ describe '.uri_checker' do
407
+ before{ ImageSize.chunk_size = 64 }
408
+ after do
409
+ ImageSize.chunk_size = nil
410
+ ImageSize.uri_checker = nil
411
+ end
412
+
413
+ it 'can be reset to the default checker' do
414
+ ImageSize.uri_checker = proc{ |uri| fail 'forbidden' if uri.port == @server.base_url.port }
415
+ ImageSize.uri_checker = nil
416
+
417
+ expect do
418
+ retry_on Timeout::Error do
419
+ ImageSize.url("#{@server.base_url}spec/images/empty")
420
+ end
421
+ end.not_to raise_error
422
+ end
423
+
424
+ it 'requires an object responding to call' do
425
+ expect{ ImageSize.uri_checker = Object.new }.to raise_error(ArgumentError)
426
+ end
427
+
428
+ it 'is checked before initial request' do
429
+ ImageSize.uri_checker = proc{ |uri| fail 'forbidden' if uri.port == @server.base_url.port }
430
+
431
+ expect do
432
+ retry_on Timeout::Error do
433
+ ImageSize.url("#{@server.base_url}spec/images/empty")
434
+ end
435
+ end.to raise_error('forbidden')
436
+ end
437
+
438
+ it 'is checked before redirect request' do
439
+ ImageSize.uri_checker = proc{ |uri| fail 'forbidden' if uri.port == @server.second_url.port }
440
+
441
+ expect do
442
+ retry_on Timeout::Error do
443
+ ImageSize.url("#{@server.base_url}spec/images/empty?redirect=1")
444
+ end
445
+ end.to raise_error('forbidden')
446
+ end
447
+ end
251
448
  end
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="430" height="430">
2
+ <g font-family="'Arial Black'" text-anchor="middle">
3
+ <text x="266.322" y="79.322" fill="#0057b7" stroke-width=".817" font-size="108.96" transform="scale(.80705 1.2391)">RUSSIAN</text>
4
+ <text x="276.58" y="163.322" fill="#0057b7" stroke-width=".794" font-size="105.9" transform="scale(.78387 1.2757)">WARSHIP</text>
5
+ <text x="268.809" y="252.317" fill="gold" stroke-width=".803" font-size="107.02" transform="scale(.79274 1.2615)">GO FUCK</text>
6
+ <text x="303.286" y="306.546" fill="gold" stroke-width=".725" font-size="96.629" transform="scale(.71575 1.3971)">YOURSELF!</text>
7
+ </g>
8
+ </svg><!-- today is day 1529 of full-scale russian invasion of Ukraine -->
9
+
10
+
11
+ <!-- pad to 768 bytes -->
data/spec/test_server.rb CHANGED
@@ -4,15 +4,37 @@ require 'webrick'
4
4
  require 'stringio'
5
5
 
6
6
  class TestServer
7
- attr_reader :base_url
7
+ class FileHandler < WEBrick::HTTPServlet::FileHandler
8
+ def service(req, res)
9
+ super
10
+ rescue WEBrick::HTTPStatus::PartialContent
11
+ if req.query['unknown_file_size']
12
+ m = %r{\Abytes (\d+)-(\d+)/\d+\z}.match(res['content-range'])
13
+ raise "Unexpected content-range: #{res['content-range']}" unless m
14
+
15
+ res['content-range'] = "bytes #{m[1]}-#{m[2]}/*"
16
+
17
+ # need to manually get the chunk, as webrick internally relies on content-range header with total size
18
+ if res.body.is_a?(IO)
19
+ offset = m[1].to_i
20
+ size = m[2].to_i - offset + 1
21
+
22
+ res.body.seek(offset, IO::SEEK_SET)
23
+ res.body = res.body.read(size)
24
+ end
25
+ end
26
+
27
+ raise
28
+ end
29
+ end
30
+
31
+ attr_reader :base_url, :second_url
8
32
 
9
33
  def initialize(host = '127.0.0.1')
10
34
  server_options = {
11
- Logger: WEBrick::Log.new(StringIO.new),
12
35
  AccessLog: [],
13
36
  BindAddress: host,
14
37
  Port: 0, # get the next available port
15
- DocumentRoot: '.',
16
38
  RequestCallback: proc do |req, res|
17
39
  redirect = req.query['redirect'].to_i
18
40
  if redirect > 0
@@ -30,7 +52,11 @@ class TestServer
30
52
  end,
31
53
  }
32
54
 
55
+ server_options[:Logger] = WEBrick::Log.new(StringIO.new) unless ENV['CI']
56
+
33
57
  @server = WEBrick::HTTPServer.new(server_options)
58
+ @server.mount('/', FileHandler, '.')
59
+
34
60
  @server.listen(host, 0) # listen on second port
35
61
 
36
62
  @base_url = URI("http://#{host}:#{@server.listeners[0].addr[1]}/")
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: image_size
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.0
4
+ version: 3.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keisuke Minami
8
8
  - Ivan Kuchin
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2024-01-16 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: rspec
@@ -56,11 +55,11 @@ dependencies:
56
55
  description: 'Measure following file dimensions: apng, avif, bmp, cur, emf, gif, heic,
57
56
  heif, ico, j2c, jp2, jpeg, jpx, mng, pam, pbm, pcx, pgm, png, ppm, psd, svg, swf,
58
57
  tiff, webp, xbm, xpm'
59
- email:
60
58
  executables: []
61
59
  extensions: []
62
60
  extra_rdoc_files: []
63
61
  files:
62
+ - ".github/dependabot.yml"
64
63
  - ".github/workflows/check.yml"
65
64
  - ".github/workflows/rubocop.yml"
66
65
  - ".gitignore"
@@ -120,6 +119,7 @@ files:
120
119
  - spec/images/pnm/ascii.22x25.ppm
121
120
  - spec/images/psd/16x20.psd
122
121
  - spec/images/svg/72x100.svg
122
+ - spec/images/svg/768b.430x430.svg
123
123
  - spec/images/svg/crlf.72x100.svg
124
124
  - spec/images/svg/long.72x100.svg
125
125
  - spec/images/svg/long.crlf.72x100.svg
@@ -140,9 +140,8 @@ licenses:
140
140
  metadata:
141
141
  bug_tracker_uri: https://github.com/toy/image_size/issues
142
142
  changelog_uri: https://github.com/toy/image_size/blob/master/CHANGELOG.markdown
143
- documentation_uri: https://www.rubydoc.info/gems/image_size/3.4.0
143
+ documentation_uri: https://www.rubydoc.info/gems/image_size/3.5.0
144
144
  source_code_uri: https://github.com/toy/image_size
145
- post_install_message:
146
145
  rdoc_options: []
147
146
  require_paths:
148
147
  - lib
@@ -157,8 +156,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
157
156
  - !ruby/object:Gem::Version
158
157
  version: '0'
159
158
  requirements: []
160
- rubygems_version: 3.4.20
161
- signing_key:
159
+ rubygems_version: 4.0.3
162
160
  specification_version: 4
163
161
  summary: Measure image size/dimensions using pure Ruby
164
162
  test_files:
@@ -199,6 +197,7 @@ test_files:
199
197
  - spec/images/pnm/ascii.22x25.ppm
200
198
  - spec/images/psd/16x20.psd
201
199
  - spec/images/svg/72x100.svg
200
+ - spec/images/svg/768b.430x430.svg
202
201
  - spec/images/svg/crlf.72x100.svg
203
202
  - spec/images/svg/long.72x100.svg
204
203
  - spec/images/svg/long.crlf.72x100.svg