image_size 3.0.2 → 3.1.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: 2e594fe6ec7b9018dd68867a5feaf557c6c22e7ed667876089fcbe35d6f3104c
4
- data.tar.gz: a221e99d54a933452712df549e2b8b05ceb85c407b27f482042d35c97dbb95c8
3
+ metadata.gz: b0873a78a9fb9f84f07d0389071379dda288330bd38ee7516b442ffde85a3af8
4
+ data.tar.gz: 0b9fffd2311efa1c24379686821289951a4cd2ec895c27325148a886a24ff57b
5
5
  SHA512:
6
- metadata.gz: dbc4b3ec562040c3316edfc7412110eec1eef87028100f156e155a65bb38bccba7199b1094daa5ddc62317229a0fbe7241af0ba2089adf6ee03ab7eb4d85c634
7
- data.tar.gz: db58b0bee4cf294cf44ac5b4afe01c8cd0420b0155e220e378755081ca1a655adb1ac30695966f150b094c648befbd8fb96f31d9a835afbefe1bffbc7ce717b4
6
+ metadata.gz: f6dede99002af9b9fc32f9776e42019079abcd846370c06b2aeee812a73c9ec46dcb6c703d1ae454f807d532c1d6b740aabb999892a6ef610cd17311f6a9a7f0
7
+ data.tar.gz: 819d8a3b7797b1f12b59eb608ffdeda9338314a923ca54711143832685853ed36b9ae8d165fcfa242ce4e1d72aecd5a03bd11714aa7aee3b464722ea8bb4688f
data/CHANGELOG.markdown CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## unreleased
4
4
 
5
+ ## v3.1.0 (2022-09-17)
6
+
7
+ * Document experimental fetching from http server [#18](https://github.com/toy/image_size/issues/18) [@toy](https://github.com/toy)
8
+ * Improve experimental fetching of image meta from http server by reading only required amount of data when server does not support range header [@toy](https://github.com/toy)
9
+
5
10
  ## v3.0.2 (2022-05-19)
6
11
 
7
12
  * Fix handling empty files [#20](https://github.com/toy/image_size/issues/20) [@toy](https://github.com/toy)
@@ -15,6 +20,7 @@
15
20
  * Read only required chunks of data for files and seekable IOs [@toy](https://github.com/toy)
16
21
  * Raise `FormatError` whenever reading data returns less data than expected [#12](https://github.com/toy/image_size/issues/12) [@toy](https://github.com/toy)
17
22
  * Add `w`/`width` and `h`/`height` accessors to `Size` [@toy](https://github.com/toy)
23
+ * Experimental efficient fetching of image meta from http server supporting range [@toy](https://github.com/toy)
18
24
 
19
25
  ## v2.1.2 (2021-08-21)
20
26
 
data/README.markdown CHANGED
@@ -1,5 +1,6 @@
1
1
  [![Gem Version](https://img.shields.io/gem/v/image_size?logo=rubygems)](https://rubygems.org/gems/image_size)
2
2
  [![Build Status](https://img.shields.io/github/workflow/status/toy/image_size/check/master?logo=github)](https://github.com/toy/image_size/actions/workflows/check.yml)
3
+ [![Rubocop](https://img.shields.io/github/workflow/status/toy/image_size/rubocop/master?label=rubocop&logo=rubocop)](https://github.com/toy/image_size/actions/workflows/rubocop.yml)
3
4
 
4
5
  # image_size
5
6
 
@@ -53,7 +54,7 @@ require 'image_size'
53
54
  image_size = ImageSize.new(ARGF)
54
55
  ```
55
56
 
56
- Works with `open-uri` if needed:
57
+ Works with `open-uri`, see [experimental HTTP server interface below](#experimental-fetch-image-meta-from-http-server):
57
58
 
58
59
  ```ruby
59
60
  require 'image_size'
@@ -89,6 +90,55 @@ File.open('spec/images/jpeg/436x429.jpeg', 'rb') do |fh|
89
90
  end
90
91
  ```
91
92
 
93
+ ### Experimental: fetch image meta from HTTP server
94
+
95
+ If server recognises Range header, only needed chunks will be fetched even for TIFF images, otherwise required amount
96
+ of data will be fetched, in most cases first few kilobytes (TIFF images is an exception).
97
+
98
+ ```ruby
99
+ require 'image_size'
100
+ require 'image_size/uri_reader'
101
+
102
+ url = 'http://upload.wikimedia.org/wikipedia/commons/b/b4/Mardin_1350660_1350692_33_images.jpg'
103
+ p ImageSize.url(url).size
104
+ ```
105
+
106
+ This interface is as fast as dedicated gem fastimage for images with meta information in the header:
107
+
108
+ ```ruby
109
+ url = 'http://upload.wikimedia.org/wikipedia/commons/b/b4/Mardin_1350660_1350692_33_images.jpg'
110
+ puts Benchmark.measure{ p FastImage.size(url) }
111
+ ```
112
+ ```
113
+ [9545, 6623]
114
+ 0.004176 0.001974 0.006150 ( 0.282889)
115
+ ```
116
+ ```ruby
117
+ puts Benchmark.measure{ p ImageSize.url(url).size }
118
+ ```
119
+ ```
120
+ [9545, 6623]
121
+ 0.005604 0.001406 0.007010 ( 0.238629)
122
+ ```
123
+
124
+ And considerably faster for images with meta information at the end of file:
125
+
126
+ ```ruby
127
+ url = "https://upload.wikimedia.org/wikipedia/commons/c/c7/Curiosity%27s_Vehicle_System_Test_Bed_%28VSTB%29_Rover_%28PIA15876%29.tif"
128
+ puts Benchmark.measure{ p FastImage.size(url) }
129
+ ```
130
+ ```
131
+ [7360, 4912]
132
+ 0.331284 0.247295 0.578579 ( 6.027051)
133
+ ```
134
+ ```ruby
135
+ puts Benchmark.measure{ p ImageSize.url(url).size }
136
+ ```
137
+ ```
138
+ [7360, 4912]
139
+ 0.006247 0.001045 0.007292 ( 0.197631)
140
+ ```
141
+
92
142
  ## Licence
93
143
 
94
144
  This code is free to use under the terms of the [Ruby's licence](LICENSE.txt).
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.0.2'
5
+ s.version = '3.1.0'
6
6
  s.summary = %q{Measure image size using pure Ruby}
7
7
  s.description = %q{Measure following file dimensions: apng, bmp, cur, gif, 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}"
@@ -6,67 +6,103 @@ require 'image_size/chunky_reader'
6
6
  require 'net/https'
7
7
  require 'uri'
8
8
 
9
- # This is a hacky experiment and not part of public API
9
+ # Experimental, not yet part of stable API
10
10
  #
11
- # It adds ability to fetch size of image from http server while downloading only
12
- # needed chunks if the server recognises Range header
11
+ # It adds ability to fetch image meta from HTTP server while downloading only
12
+ # needed chunks if the server recognises Range header, otherwise fetches only
13
+ # required amount of data
13
14
  class ImageSize
14
- class URIReader # :nodoc:
15
- include ChunkyReader
16
-
17
- def initialize(uri, redirects = 5)
18
- if !@http || @http.address != uri.host || @http.port != uri.port
19
- @http.finish if @http
20
- @http = Net::HTTP.new(uri.host, uri.port)
21
- @http.use_ssl = true if uri.scheme == 'https'
22
- @http.start
23
- end
15
+ module URIReader # :nodoc:
16
+ module HTTPChunkyReader # :nodoc:
17
+ include ChunkyReader
24
18
 
25
- @request_uri = uri.request_uri
26
- response = request_chunk(0)
27
-
28
- case response
29
- when Net::HTTPRedirection
30
- raise "Too many redirects: #{response['location']}" unless redirects > 0
31
-
32
- initialize(uri + response['location'], redirects - 1)
33
- when Net::HTTPOK
34
- @body = response.body
35
- when Net::HTTPPartialContent
36
- @chunks = { 0 => response.body }
37
- when Net::HTTPRequestedRangeNotSatisfiable
38
- @body = ''
39
- else
40
- raise "Unexpected response: #{response}"
19
+ def chunk_range_header(i)
20
+ { 'Range' => "bytes=#{chunk_size * i}-#{(chunk_size * (i + 1)) - 1}" }
41
21
  end
42
22
  end
43
23
 
44
- def [](offset, length)
45
- if @body
24
+ class BodyReader # :nodoc:
25
+ include ChunkyReader
26
+
27
+ def initialize(response)
28
+ @body = String.new
29
+ @body_reader = response.to_enum(:read_body)
30
+ end
31
+
32
+ def [](offset, length)
33
+ if @body_reader
34
+ begin
35
+ @body << @body_reader.next while @body.length < offset + length
36
+ rescue StopIteration, IOError
37
+ @body_reader = nil
38
+ end
39
+ end
40
+
46
41
  @body[offset, length]
47
- else
48
- super
49
42
  end
50
43
  end
51
44
 
52
- def chunk(i)
53
- unless @chunks.key?(i)
54
- response = request_chunk(i)
55
- case response
56
- when Net::HTTPPartialContent
57
- @chunks[i] = response.body
58
- else
59
- raise "Unexpected response: #{response}"
60
- end
45
+ class RangeReader # :nodoc:
46
+ include HTTPChunkyReader
47
+
48
+ def initialize(http, request_uri, chunk0)
49
+ @http = http
50
+ @request_uri = request_uri
51
+ @chunks = { 0 => chunk0 }
61
52
  end
62
53
 
63
- @chunks[i]
54
+ def chunk(i)
55
+ unless @chunks.key?(i)
56
+ response = @http.get(@request_uri, chunk_range_header(i))
57
+ case response
58
+ when Net::HTTPPartialContent
59
+ @chunks[i] = response.body
60
+ else
61
+ raise "Unexpected response: #{response}"
62
+ end
63
+ end
64
+
65
+ @chunks[i]
66
+ end
64
67
  end
65
68
 
66
- private
69
+ class << self
70
+ include HTTPChunkyReader
71
+
72
+ def open(uri, max_redirects = 5)
73
+ http = nil
74
+ (max_redirects + 1).times do
75
+ unless http && http.address == uri.host && http.port == uri.port
76
+ http.finish if http
77
+
78
+ http = Net::HTTP.new(uri.host, uri.port)
79
+ http.use_ssl = true if uri.scheme == 'https'
80
+ http.start
81
+ end
67
82
 
68
- def request_chunk(i)
69
- @http.get(@request_uri, 'Range' => "bytes=#{chunk_size * i}-#{(chunk_size * (i + 1)) - 1}")
83
+ response = http.request_get(uri.request_uri, chunk_range_header(0)) do |response_with_unread_body|
84
+ case response_with_unread_body
85
+ when Net::HTTPOK
86
+ return yield BodyReader.new(response_with_unread_body)
87
+ end
88
+ end
89
+
90
+ case response
91
+ when Net::HTTPRedirection
92
+ uri += response['location']
93
+ when Net::HTTPPartialContent
94
+ return yield RangeReader.new(http, uri.request_uri, response.body)
95
+ when Net::HTTPRequestedRangeNotSatisfiable
96
+ return yield StringReader.new('')
97
+ else
98
+ raise "Unexpected response: #{response}"
99
+ end
100
+ end
101
+
102
+ raise "Too many redirects: #{uri}"
103
+ ensure
104
+ http.finish if http.started?
105
+ end
70
106
  end
71
107
  end
72
108
 
@@ -74,7 +110,7 @@ class ImageSize
74
110
  class << self
75
111
  def open_with_uri(input, &block)
76
112
  if input.is_a?(URI)
77
- yield URIReader.new(input)
113
+ URIReader.open(input, &block)
78
114
  else
79
115
  open_without_uri(input, &block)
80
116
  end
@@ -56,9 +56,11 @@ describe ImageSize::ChunkyReader do
56
56
  offsets.each do |offset_b|
57
57
  length = offset_b - offset
58
58
  expect(reader[offset, length]).to eq(data[offset, length]),
59
- "for offset #{offset} and length #{length}\n"\
60
- "expected: #{data[offset, length].inspect}\n"\
61
- " got: #{reader[offset, length].inspect}"
59
+ [
60
+ "for offset #{offset} and length #{length}",
61
+ "expected: #{data[offset, length].inspect}",
62
+ " got: #{reader[offset, length].inspect}",
63
+ ].join("\n")
62
64
  end
63
65
  end
64
66
  end
@@ -7,24 +7,16 @@ require 'image_size/uri_reader'
7
7
 
8
8
  require 'tempfile'
9
9
  require 'shellwords'
10
- require 'webrick'
10
+
11
+ require 'test_server'
11
12
 
12
13
  describe ImageSize do
13
14
  before :all do
14
- @server = WEBrick::HTTPServer.new({
15
- :Logger => WEBrick::Log.new(StringIO.new),
16
- :AccessLog => [],
17
- :BindAddress => '127.0.0.1',
18
- :Port => 0, # get the next available port
19
- :DocumentRoot => '.',
20
- })
21
- @server_thread = Thread.new{ @server.start }
22
- @server_base_url = URI("http://127.0.0.1:#{@server.config[:Port]}/")
15
+ @server = TestServer.new
23
16
  end
24
17
 
25
18
  after :all do
26
- @server.shutdown
27
- @server_thread.join
19
+ @server.finish
28
20
  end
29
21
 
30
22
  Dir['spec/**/*'].each do |path|
@@ -131,9 +123,54 @@ describe ImageSize do
131
123
  end
132
124
 
133
125
  context 'fetching from webserver' do
134
- it 'gets format and dimensions' do
135
- image_size = ImageSize.url(@server_base_url + path)
136
- expect(image_size).to have_attributes(attributes)
126
+ let(:file_url){ @server.base_url + path }
127
+
128
+ context 'supporting range' do
129
+ context 'without redirects' do
130
+ it 'gets format and dimensions' do
131
+ image_size = ImageSize.url(file_url)
132
+ expect(image_size).to have_attributes(attributes)
133
+ end
134
+ end
135
+
136
+ context 'with redirects' do
137
+ it 'gets format and dimensions' do
138
+ image_size = ImageSize.url("#{file_url}?redirect=5")
139
+ expect(image_size).to have_attributes(attributes)
140
+ end
141
+ end
142
+
143
+ context 'with too many redirects' do
144
+ it 'gets format and dimensions' do
145
+ expect do
146
+ ImageSize.url("#{file_url}?redirect=6")
147
+ end.to raise_error(/Too many redirects/)
148
+ end
149
+ end
150
+ end
151
+
152
+ context 'not supporting range' do
153
+ context 'without redirects' do
154
+ it 'gets format and dimensions' do
155
+ image_size = ImageSize.url("#{file_url}?ignore_range")
156
+ expect(image_size).to have_attributes(attributes)
157
+ end
158
+ end
159
+
160
+ context 'with redirects' do
161
+ it 'gets format and dimensions' do
162
+ image_size = ImageSize.url("#{file_url}?ignore_range&redirect=5")
163
+ expect(image_size).to have_attributes(attributes)
164
+ end
165
+ end
166
+
167
+ context 'with too many redirects' do
168
+ it 'gets format and dimensions' do
169
+ expect do
170
+ ImageSize.url("#{file_url}?ignore_range&redirect=6")
171
+ end.to raise_error(/Too many redirects/)
172
+ end
173
+ end
137
174
  end
138
175
  end
139
176
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'webrick'
4
+
5
+ class TestServer
6
+ attr_reader :base_url
7
+
8
+ def initialize(host = '127.0.0.1')
9
+ server_options = {
10
+ :Logger => WEBrick::Log.new(StringIO.new),
11
+ :AccessLog => [],
12
+ :BindAddress => host,
13
+ :Port => 0, # get the next available port
14
+ :DocumentRoot => '.',
15
+ :RequestCallback => proc do |req, res|
16
+ redirect = req.query['redirect'].to_i
17
+ if redirect > 0
18
+ res.set_redirect(
19
+ WEBrick::HTTPStatus::TemporaryRedirect,
20
+ [
21
+ req.request_uri.port == @base_url.port ? @second_url : @base_url,
22
+ req.request_uri.request_uri,
23
+ "?#{encode_www_form(req.query.merge('redirect' => redirect - 1))}",
24
+ ].inject(:+)
25
+ )
26
+ end
27
+
28
+ req.header.delete('range') if req.query['ignore_range']
29
+ end,
30
+ }
31
+
32
+ @server = WEBrick::HTTPServer.new(server_options)
33
+ @server.listen(host, 0) # listen on second port
34
+
35
+ @base_url = URI("http://#{host}:#{@server.listeners[0].addr[1]}/")
36
+ @second_url = URI("http://#{host}:#{@server.listeners[1].addr[1]}/")
37
+
38
+ @thread = Thread.new{ @server.start }
39
+ end
40
+
41
+ def finish
42
+ @server.shutdown
43
+ @thread.join
44
+ end
45
+
46
+ private
47
+
48
+ if URI.respond_to?(:encode_www_form)
49
+ def encode_www_form(h)
50
+ URI.encode_www_form(h)
51
+ end
52
+ else
53
+ require 'cgi'
54
+
55
+ def encode_www_form(h)
56
+ h.map do |k, v|
57
+ "#{CGI.escape(k)}=#{CGI.escape(v.to_s)}"
58
+ end.join('&')
59
+ end
60
+ end
61
+ end
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.0.2
4
+ version: 3.1.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: 2022-05-19 00:00:00.000000000 Z
12
+ date: 2022-09-16 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
@@ -120,13 +120,14 @@ files:
120
120
  - spec/images/xbm/crlf.16x32.xbm
121
121
  - spec/images/xpm/24x32.xpm
122
122
  - spec/images/xpm/crlf.24x32.xpm
123
+ - spec/test_server.rb
123
124
  homepage: https://github.com/toy/image_size
124
125
  licenses:
125
126
  - Ruby
126
127
  metadata:
127
128
  bug_tracker_uri: https://github.com/toy/image_size/issues
128
129
  changelog_uri: https://github.com/toy/image_size/blob/master/CHANGELOG.markdown
129
- documentation_uri: https://www.rubydoc.info/gems/image_size/3.0.2
130
+ documentation_uri: https://www.rubydoc.info/gems/image_size/3.1.0
130
131
  source_code_uri: https://github.com/toy/image_size
131
132
  post_install_message:
132
133
  rdoc_options: []
@@ -190,3 +191,4 @@ test_files:
190
191
  - spec/images/xbm/crlf.16x32.xbm
191
192
  - spec/images/xpm/24x32.xpm
192
193
  - spec/images/xpm/crlf.24x32.xpm
194
+ - spec/test_server.rb