image_size 3.0.2 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
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