image_size 3.4.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d88e4be6d69bedc0f976c9b73544a20ac08ab66d6b44ce1061d1374f9d2a9dc8
4
- data.tar.gz: 5b2986943b8bbeb339d0c31d0a98c5847a1a86141d5e754adaf0bd01e3d8d60f
3
+ metadata.gz: e7880ff3fdbcfdbbc6e3248c79ef59adea338f21c63c285323c5b01bdf699477
4
+ data.tar.gz: b8ce40e97bc91a2f7e91a9ce3f47d17f06aa38a6a28f7e534837c1f668cc51e2
5
5
  SHA512:
6
- metadata.gz: 1b75df7c37cbcca0886eade3e47b082cde0468331c954d0605130cba564bb7718d777628599fea7332e2ae1ef9f15b80ce77072352a109dadbf9749fea049572
7
- data.tar.gz: ffc57b47280776fc7be3cb2866699f32f90a2e036b2dca703ac4262aa77750032ffb54700437958ba7b4948c934d5ea6e41f2acad4e7e71ddec84a98f18edabf
6
+ metadata.gz: 027e0f32e86acf19b92cb00404dfb2b26f0989ed13d07247512ff024d058a245d62b3b252e2605ff1d79a41636cb87874f2f556f3e2cdcaf2727efe3fc91efe2
7
+ data.tar.gz: b8a5074adaa1d08d6b3b8d9d1a5da5c5239e1ea46b345fc13ffbf5ba75736d826beaeceb38b09aec7267f6c934856c1237c9c514ecad3fb6e9701b0846352f0d
@@ -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
@@ -4,12 +4,17 @@ on:
4
4
  pull_request:
5
5
  schedule:
6
6
  - cron: 45 4 * * 3
7
+ permissions:
8
+ contents: read
7
9
  jobs:
8
10
  check:
9
- runs-on: ubuntu-latest
11
+ runs-on: ubuntu-22.04
10
12
  strategy:
11
13
  matrix:
12
14
  ruby:
15
+ - '1.9.3'
16
+ - '2.0'
17
+ - '2.1'
13
18
  - '2.3'
14
19
  - '2.4'
15
20
  - '2.5'
@@ -19,31 +24,18 @@ jobs:
19
24
  - '3.1'
20
25
  - '3.2'
21
26
  - '3.3'
22
- - jruby-9.3
27
+ - '3.4'
28
+ - '4.0'
23
29
  - jruby-9.4
30
+ - jruby-10.1
24
31
  fail-fast: false
25
32
  steps:
26
- - uses: actions/checkout@v3
33
+ - uses: actions/checkout@v6
27
34
  - uses: ruby/setup-ruby@v1
28
35
  with:
29
36
  ruby-version: "${{ matrix.ruby }}"
30
37
  bundler-cache: true
31
38
  - 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
39
  windows:
48
40
  runs-on: windows-latest
49
41
  strategy:
@@ -55,9 +47,11 @@ jobs:
55
47
  - '3.1'
56
48
  - '3.2'
57
49
  - '3.3'
50
+ - '3.4'
51
+ - '4.0'
58
52
  fail-fast: false
59
53
  steps:
60
- - uses: actions/checkout@v3
54
+ - uses: actions/checkout@v6
61
55
  - uses: ruby/setup-ruby@v1
62
56
  with:
63
57
  ruby-version: "${{ matrix.ruby }}"
@@ -4,13 +4,15 @@ on:
4
4
  pull_request:
5
5
  schedule:
6
6
  - cron: 45 4 * * 3
7
+ permissions:
8
+ contents: read
7
9
  jobs:
8
10
  rubocop:
9
11
  runs-on: ubuntu-latest
10
12
  steps:
11
- - uses: actions/checkout@v3
13
+ - uses: actions/checkout@v6
12
14
  - uses: ruby/setup-ruby@v1
13
15
  with:
14
- ruby-version: '3'
16
+ ruby-version: '4'
15
17
  bundler-cache: true
16
18
  - 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,24 @@
2
2
 
3
3
  ## unreleased
4
4
 
5
+ ## v3.6.0 (2026-05-26)
6
+
7
+ * Support `.icns` Apple Icon Image format [@toy](https://github.com/toy)
8
+
9
+ ## v3.5.0 (2026-05-03)
10
+
11
+ * 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)
12
+ * Add minimal validation of `dpi` global configuration [@toy](https://github.com/toy)
13
+ * Make `chunk_size` globally configurable [@toy](https://github.com/toy)
14
+ * Make `max_redirects` globally configurable [@toy](https://github.com/toy)
15
+ * Prevent requesting chunks over http after end of file [#29](https://github.com/toy/image_size/issues/29) [@toy](https://github.com/toy)
16
+ * Add ability to restrict fetched URIs by setting `uri_checker` proc [@toy](https://github.com/toy)
17
+
5
18
  ## v3.4.0 (2024-01-16)
6
19
 
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)
20
+ * 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)
21
+ * Allow fetching from HTTP server by requiring `image_size/uri` [@toy](https://github.com/toy)
22
+ * Fix for `ArgumentError` when requiring only `image_size/uri_reader` (without image_size) [@toy](https://github.com/toy)
10
23
  * Require ruby 1.9.3 [@toy](https://github.com/toy)
11
24
 
12
25
  ## v3.3.0 (2023-05-30)
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/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
6
6
 
7
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`.
8
+ Formats: `apng`, `avif`, `bmp`, `cur`, `emf`, `gif`, `heic`, `heif`, `icns`, `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
 
@@ -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,9 +2,9 @@
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.6.0'
6
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}
7
+ s.description = %q{Measure following file dimensions: apng, avif, bmp, cur, emf, gif, heic, heif, icns, 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'
@@ -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 = {
@@ -9,6 +9,7 @@ class ImageSize
9
9
  emf: %w[image/emf],
10
10
  gif: %w[image/gif],
11
11
  heic: %w[image/heic image/heif],
12
+ icns: %w[image/x-icns],
12
13
  ico: %w[image/x-icon image/vnd.microsoft.icon],
13
14
  j2c: %w[image/j2c],
14
15
  jp2: %w[image/jp2],
@@ -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