fastimage 1.8.1 → 2.2.3
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 +5 -5
- data/README.textile +34 -31
- data/lib/fastimage.rb +274 -66
- metadata +11 -61
- data/lib/fastimage/fbr.rb +0 -66
- data/test/fixtures/bad.jpg +0 -0
- data/test/fixtures/exif_orientation.jpg +0 -0
- data/test/fixtures/faulty.jpg +0 -0
- data/test/fixtures/folder with spaces/test.bmp +0 -0
- data/test/fixtures/gzipped.jpg +0 -0
- data/test/fixtures/infinite.jpg +0 -0
- data/test/fixtures/man.ico +0 -0
- data/test/fixtures/orient_2.jpg +0 -0
- data/test/fixtures/test.bmp +0 -0
- data/test/fixtures/test.cur +0 -0
- data/test/fixtures/test.gif +0 -0
- data/test/fixtures/test.jpg +0 -0
- data/test/fixtures/test.png +0 -0
- data/test/fixtures/test.psd +0 -0
- data/test/fixtures/test.svg +0 -43
- data/test/fixtures/test.tiff +0 -0
- data/test/fixtures/test2.bmp +0 -0
- data/test/fixtures/test2.jpg +0 -0
- data/test/fixtures/test2.tiff +0 -0
- data/test/fixtures/test3.jpg +0 -0
- data/test/fixtures/test4.jpg +0 -0
- data/test/fixtures/test_partial_viewport.svg +0 -9
- data/test/fixtures/truncated_gzipped.jpg +0 -0
- data/test/fixtures/webp_vp8.webp +0 -0
- data/test/fixtures/webp_vp8l.webp +0 -0
- data/test/fixtures/webp_vp8x.webp +0 -0
- data/test/test.rb +0 -323
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4124655c4f8af69e9b8bf9bfa433a11161e9ea97c23e755a196555012eadf75b
|
4
|
+
data.tar.gz: 40fef6aa908fc14799f2cb53e80d5081bef11ce0482d86c5947841fb29ee9604
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 205fb43dacd3239f8425dd8cbd311be8c375f3c377cbf71771b87304b0c62780905a366783b1b5606e7386edf9ed2fe1cab138b8a20ad47f238150d5143b93db
|
7
|
+
data.tar.gz: 219ac67b55c23bb59458fc57d165a76c0bf5855c971c705d033103c01848cbb9f3fb4a1e8024419192ced93f076d223fb6e3ce77289b8ed94858d0e58c5387df
|
data/README.textile
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
!https://
|
1
|
+
!https://img.shields.io/gem/dt/fastimage.svg!:https://rubygems.org/gems/fastimage
|
2
|
+
!https://travis-ci.org/sdsykes/fastimage.svg?branch=master!:https://travis-ci.org/sdsykes/fastimage
|
2
3
|
|
3
4
|
h1. FastImage
|
4
5
|
|
@@ -18,27 +19,29 @@ You only need supply the uri, and FastImage will do the rest.
|
|
18
19
|
|
19
20
|
h2. Features
|
20
21
|
|
21
|
-
|
22
|
-
interpreted as a filename, and FastImage will attempt to open it with File#open.
|
22
|
+
FastImage can also read local (and other) files - anything that is not parseable as a URI will be interpreted as a filename, and FastImage will attempt to open it with @File#open@.
|
23
23
|
|
24
|
-
FastImage will also automatically read from any object that responds to
|
25
|
-
instance an IO object if that is passed instead of a URI.
|
24
|
+
FastImage will also automatically read from any object that responds to @:read@ - for instance an IO object if that is passed instead of a URI.
|
26
25
|
|
27
26
|
FastImage will follow up to 4 HTTP redirects to get the image.
|
28
27
|
|
29
|
-
FastImage will obey the http_proxy setting in your environment to route requests via a proxy. You can also pass a
|
28
|
+
FastImage will obey the @http_proxy@ setting in your environment to route requests via a proxy. You can also pass a @:proxy@ argument if you want to specify the proxy address in the call.
|
30
29
|
|
31
|
-
You can add a timeout to the request which will limit the request time by passing
|
30
|
+
You can add a timeout to the request which will limit the request time by passing @:timeout => number_of_seconds@.
|
32
31
|
|
33
|
-
FastImage normally replies
|
32
|
+
FastImage normally replies with @nil@ if it encounters an error, but you can pass @:raise_on_failure => true@ to get an exception.
|
34
33
|
|
35
34
|
FastImage also provides a reader for the content length header provided in HTTP. This may be useful to assess the file size of an image, but do not rely on it exclusively - it will not be present in chunked responses for instance.
|
36
35
|
|
37
|
-
FastImage accepts additional HTTP headers. This can be used to set a user agent or referrer which some servers require. Pass an
|
36
|
+
FastImage accepts additional HTTP headers. This can be used to set a user agent or referrer which some servers require. Pass an @:http_header@ argument to specify headers, e.g., @:http_header => {'User-Agent' => 'Fake Browser'}@.
|
37
|
+
|
38
|
+
FastImage can give you information about the parsed display orientation of an image with Exif data (jpeg or tiff).
|
39
|
+
|
40
|
+
FastImage also handles "Data URIs":https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs correctly.
|
38
41
|
|
39
42
|
h2. Security
|
40
43
|
|
41
|
-
As of v1.6.7 FastImage no longer uses openuri to open files, but directly calls File.open
|
44
|
+
As of v1.6.7 FastImage no longer uses @openuri@ to open files, but directly calls @File.open@. Take care to sanitise the strings passed to FastImage; it will try to read from whatever is passed.
|
42
45
|
|
43
46
|
h2. Examples
|
44
47
|
|
@@ -59,10 +62,20 @@ FastImage.new("http://stephensykes.com/images/pngimage").content_length
|
|
59
62
|
=> 432
|
60
63
|
FastImage.size("http://stephensykes.com/images/ss.com_x.gif", :http_header => {'User-Agent' => 'Fake Browser'})
|
61
64
|
=> [266, 56]
|
65
|
+
FastImage.new("http://stephensykes.com/images/ExifOrientation3.jpg").orientation
|
66
|
+
=> 3
|
67
|
+
FastImage.size("data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==")
|
68
|
+
=> [1, 1]
|
62
69
|
</code></pre>
|
63
70
|
|
64
71
|
h2. Installation
|
65
72
|
|
73
|
+
h4. Required Ruby version
|
74
|
+
|
75
|
+
FastImage version 2.0.0 and above work with Ruby 1.9.2 and above.
|
76
|
+
|
77
|
+
FastImage version 1.9.0 was the last version that supported Ruby 1.8.7.
|
78
|
+
|
66
79
|
h4. Gem
|
67
80
|
|
68
81
|
bc. gem install fastimage
|
@@ -79,6 +92,10 @@ h2. Documentation
|
|
79
92
|
|
80
93
|
"http://sdsykes.github.io/fastimage/rdoc/FastImage.html":http://sdsykes.github.io/fastimage/rdoc/FastImage.html
|
81
94
|
|
95
|
+
h2. Maintainer
|
96
|
+
|
97
|
+
FastImage is maintained by Stephen Sykes (@sdsykes). Support this project by using "LibPixel":https://libpixel.com cloud based image resizing and processing service.
|
98
|
+
|
82
99
|
h2. Benchmark
|
83
100
|
|
84
101
|
It's way faster than conventional methods (for example the image_size gem) for most types of file when fetching over the wire.
|
@@ -88,7 +105,7 @@ irb> uri = "http://upload.wikimedia.org/wikipedia/commons/b/b4/Mardin_1350660_13
|
|
88
105
|
irb> puts Benchmark.measure {open(uri, 'rb') {|fh| p ImageSize.new(fh).size}}
|
89
106
|
[9545, 6623]
|
90
107
|
0.680000 0.250000 0.930000 ( 7.571887)
|
91
|
-
|
108
|
+
|
92
109
|
irb> puts Benchmark.measure {p FastImage.size(uri)}
|
93
110
|
[9545, 6623]
|
94
111
|
0.010000 0.000000 0.010000 ( 0.090640)
|
@@ -105,7 +122,7 @@ irb> uri = "http://upload.wikimedia.org/wikipedia/commons/1/11/Shinbutsureijoush
|
|
105
122
|
irb> puts Benchmark.measure {open(uri, 'rb') {|fh| p ImageSize.new(fh).size}}
|
106
123
|
[1120, 1559]
|
107
124
|
1.080000 0.370000 1.450000 ( 13.766962)
|
108
|
-
|
125
|
+
|
109
126
|
irb> puts Benchmark.measure {p FastImage.size(uri)}
|
110
127
|
[1120, 1559]
|
111
128
|
3.490000 3.810000 7.300000 ( 11.754315)
|
@@ -115,18 +132,17 @@ h2. Tests
|
|
115
132
|
|
116
133
|
You'll need to @gem install fakeweb@ and possibly also @gem install test-unit@ to be able to run the tests.
|
117
134
|
|
118
|
-
bc.. $ ruby test.rb
|
119
|
-
Run options:
|
135
|
+
bc.. $ ruby test/test.rb
|
136
|
+
Run options:
|
120
137
|
|
121
138
|
# Running tests:
|
122
139
|
|
123
|
-
Finished tests in 1.033640s, 23.2189 tests/s, 82.2337 assertions/s.
|
140
|
+
Finished tests in 1.033640s, 23.2189 tests/s, 82.2337 assertions/s.
|
124
141
|
24 tests, 85 assertions, 0 failures, 0 errors, 0 skips
|
125
142
|
|
126
143
|
h2. References
|
127
144
|
|
128
145
|
* "Pennysmalls - Find jpeg dimensions fast in pure Ruby, no image library needed":http://pennysmalls.wordpress.com/2008/08/19/find-jpeg-dimensions-fast-in-pure-ruby-no-ima/
|
129
|
-
* "DZone - Determine Image Size":http://snippets.dzone.com/posts/show/805
|
130
146
|
* "Antti Kupila - Getting JPG dimensions with AS3 without loading the entire file":http://www.anttikupila.com/flash/getting-jpg-dimensions-with-as3-without-loading-the-entire-file/
|
131
147
|
* "imagesize gem":https://rubygems.org/gems/imagesize
|
132
148
|
* "EXIF Reader":https://github.com/remvee/exifr
|
@@ -139,22 +155,9 @@ h2. FastImage in other languages
|
|
139
155
|
* "PHP by tommoor":https://github.com/tommoor/fastimage
|
140
156
|
* "Node.js by ShogunPanda":https://github.com/ShogunPanda/fastimage
|
141
157
|
* "Objective C by kylehickinson":https://github.com/kylehickinson/FastImage
|
158
|
+
* "Android by qstumn":https://github.com/qstumn/FastImageSize
|
159
|
+
* "Flutter by ky1vstar":https://github.com/ky1vstar/fastimage.dart
|
142
160
|
|
143
161
|
h2. Licence
|
144
162
|
|
145
163
|
MIT, see file "MIT-LICENSE":MIT-LICENSE
|
146
|
-
|
147
|
-
h2. Contributors
|
148
|
-
|
149
|
-
Pull requests and suggestions are always welcome. Thanks to all the contributors!
|
150
|
-
|
151
|
-
* @felixbuenemann
|
152
|
-
* @speedmax
|
153
|
-
* @sebastianludwig
|
154
|
-
* @benjaminjackson
|
155
|
-
* @muffinista
|
156
|
-
* @marcandre
|
157
|
-
* @apanzerj
|
158
|
-
* @forresty
|
159
|
-
* kikihakiem
|
160
|
-
* lulalalalistia
|
data/lib/fastimage.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
# coding: ASCII-8BIT
|
2
3
|
|
3
4
|
# FastImage finds the size or type of an image given its uri.
|
@@ -28,6 +29,9 @@
|
|
28
29
|
# or referrer which some servers require. Pass an :http_header argument to specify headers,
|
29
30
|
# e.g., :http_header => {'User-Agent' => 'Fake Browser'}.
|
30
31
|
#
|
32
|
+
# FastImage can give you information about the parsed display orientation of an image with Exif
|
33
|
+
# data (jpeg or tiff).
|
34
|
+
#
|
31
35
|
# === Examples
|
32
36
|
# require 'fastimage'
|
33
37
|
#
|
@@ -39,9 +43,12 @@
|
|
39
43
|
# => :gif
|
40
44
|
# File.open("/some/local/file.gif", "r") {|io| FastImage.type(io)}
|
41
45
|
# => :gif
|
46
|
+
# FastImage.new("http://stephensykes.com/images/pngimage").content_length
|
47
|
+
# => 432
|
48
|
+
# FastImage.new("http://stephensykes.com/images/ExifOrientation3.jpg").orientation
|
49
|
+
# => 3
|
42
50
|
#
|
43
51
|
# === References
|
44
|
-
# * http://snippets.dzone.com/posts/show/805
|
45
52
|
# * http://www.anttikupila.com/flash/getting-jpg-dimensions-with-as3-without-loading-the-entire-file/
|
46
53
|
# * http://pennysmalls.wordpress.com/2008/08/19/find-jpeg-dimensions-fast-in-pure-ruby-no-ima/
|
47
54
|
# * https://rubygems.org/gems/imagesize
|
@@ -49,14 +56,21 @@
|
|
49
56
|
#
|
50
57
|
|
51
58
|
require 'net/https'
|
52
|
-
require 'addressable/uri'
|
53
|
-
require 'fastimage/fbr.rb'
|
54
59
|
require 'delegate'
|
55
60
|
require 'pathname'
|
56
61
|
require 'zlib'
|
62
|
+
require 'base64'
|
63
|
+
require 'uri'
|
64
|
+
|
65
|
+
# see http://stackoverflow.com/questions/5208851/i/41048816#41048816
|
66
|
+
if RUBY_VERSION < "2.2"
|
67
|
+
module URI
|
68
|
+
DEFAULT_PARSER = Parser.new(:HOSTNAME => "(?:(?:[a-zA-Z\\d](?:[-\\_a-zA-Z\\d]*[a-zA-Z\\d])?)\\.)*(?:[a-zA-Z](?:[-\\_a-zA-Z\\d]*[a-zA-Z\\d])?)\\.?")
|
69
|
+
end
|
70
|
+
end
|
57
71
|
|
58
72
|
class FastImage
|
59
|
-
attr_reader :size, :type, :content_length
|
73
|
+
attr_reader :size, :type, :content_length, :orientation, :animated
|
60
74
|
|
61
75
|
attr_reader :bytes_read
|
62
76
|
|
@@ -165,6 +179,34 @@ class FastImage
|
|
165
179
|
new(uri, options.merge(:type_only=>true)).type
|
166
180
|
end
|
167
181
|
|
182
|
+
# Returns a boolean value indicating the image is animated.
|
183
|
+
# It will return nil if the image could not be fetched, or if the image type was not recognised.
|
184
|
+
#
|
185
|
+
# By default there is a timeout of 2 seconds for opening and reading from a remote server.
|
186
|
+
# This can be changed by passing a :timeout => number_of_seconds in the options.
|
187
|
+
#
|
188
|
+
# If you wish FastImage to raise if it cannot find the type of the image for any reason, then pass
|
189
|
+
# :raise_on_failure => true in the options.
|
190
|
+
#
|
191
|
+
# === Example
|
192
|
+
#
|
193
|
+
# require 'fastimage'
|
194
|
+
#
|
195
|
+
# FastImage.animated?("test/fixtures/test.gif")
|
196
|
+
# => false
|
197
|
+
# FastImage.animated?("test/fixtures/animated.gif")
|
198
|
+
# => true
|
199
|
+
#
|
200
|
+
# === Supported options
|
201
|
+
# [:timeout]
|
202
|
+
# Overrides the default timeout of 2 seconds. Applies both to reading from and opening the http connection.
|
203
|
+
# [:raise_on_failure]
|
204
|
+
# If set to true causes an exception to be raised if the image type cannot be found for any reason.
|
205
|
+
#
|
206
|
+
def self.animated?(uri, options={})
|
207
|
+
new(uri, options.merge(:animated_only=>true)).animated
|
208
|
+
end
|
209
|
+
|
168
210
|
def initialize(uri, options={})
|
169
211
|
@uri = uri
|
170
212
|
@options = {
|
@@ -175,14 +217,24 @@ class FastImage
|
|
175
217
|
:http_header => {}
|
176
218
|
}.merge(options)
|
177
219
|
|
178
|
-
@property = @options[:
|
220
|
+
@property = if @options[:animated_only]
|
221
|
+
:animated
|
222
|
+
elsif @options[:type_only]
|
223
|
+
:type
|
224
|
+
else
|
225
|
+
:size
|
226
|
+
end
|
227
|
+
|
228
|
+
@type, @state = nil
|
179
229
|
|
180
230
|
if uri.respond_to?(:read)
|
181
231
|
fetch_using_read(uri)
|
232
|
+
elsif uri.start_with?('data:')
|
233
|
+
fetch_using_base64(uri)
|
182
234
|
else
|
183
235
|
begin
|
184
|
-
@parsed_uri =
|
185
|
-
rescue
|
236
|
+
@parsed_uri = URI.parse(uri)
|
237
|
+
rescue URI::InvalidURIError
|
186
238
|
fetch_using_file_open
|
187
239
|
else
|
188
240
|
if @parsed_uri.scheme == "http" || @parsed_uri.scheme == "https"
|
@@ -193,17 +245,14 @@ class FastImage
|
|
193
245
|
end
|
194
246
|
end
|
195
247
|
|
196
|
-
uri.rewind if uri.respond_to?(:rewind)
|
197
|
-
|
198
248
|
raise SizeNotFound if @options[:raise_on_failure] && @property == :size && !@size
|
199
249
|
|
200
250
|
rescue Timeout::Error, SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ECONNRESET,
|
201
|
-
ImageFetchFailure, Net::HTTPBadResponse, EOFError, Errno::ENOENT
|
202
|
-
|
203
|
-
rescue NoMethodError # 1.8.7p248 can raise this due to a net/http bug
|
251
|
+
Errno::ENETUNREACH, ImageFetchFailure, Net::HTTPBadResponse, EOFError, Errno::ENOENT,
|
252
|
+
OpenSSL::SSL::SSLError
|
204
253
|
raise ImageFetchFailure if @options[:raise_on_failure]
|
205
254
|
rescue UnknownImageType
|
206
|
-
raise
|
255
|
+
raise if @options[:raise_on_failure]
|
207
256
|
rescue CannotParseImage
|
208
257
|
if @options[:raise_on_failure]
|
209
258
|
if @property == :size
|
@@ -213,6 +262,9 @@ class FastImage
|
|
213
262
|
end
|
214
263
|
end
|
215
264
|
|
265
|
+
ensure
|
266
|
+
uri.rewind if uri.respond_to?(:rewind)
|
267
|
+
|
216
268
|
end
|
217
269
|
|
218
270
|
private
|
@@ -223,6 +275,17 @@ class FastImage
|
|
223
275
|
fetch_using_http_from_parsed_uri
|
224
276
|
end
|
225
277
|
|
278
|
+
# Some invalid locations need escaping
|
279
|
+
def escaped_location(location)
|
280
|
+
begin
|
281
|
+
URI(location)
|
282
|
+
rescue URI::InvalidURIError
|
283
|
+
::URI::DEFAULT_PARSER.escape(location)
|
284
|
+
else
|
285
|
+
location
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
226
289
|
def fetch_using_http_from_parsed_uri
|
227
290
|
http_header = {'Accept-Encoding' => 'identity'}.merge(@options[:http_header])
|
228
291
|
|
@@ -231,14 +294,11 @@ class FastImage
|
|
231
294
|
if res.is_a?(Net::HTTPRedirection) && @redirect_count < 4
|
232
295
|
@redirect_count += 1
|
233
296
|
begin
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
@parsed_uri = newly_parsed_uri
|
240
|
-
end
|
241
|
-
rescue Addressable::URI::InvalidURIError
|
297
|
+
location = res['Location']
|
298
|
+
raise ImageFetchFailure if location.nil? || location.empty?
|
299
|
+
|
300
|
+
@parsed_uri = URI.join(@parsed_uri, escaped_location(location))
|
301
|
+
rescue URI::InvalidURIError
|
242
302
|
else
|
243
303
|
fetch_using_http_from_parsed_uri
|
244
304
|
break
|
@@ -276,14 +336,18 @@ class FastImage
|
|
276
336
|
end
|
277
337
|
end
|
278
338
|
|
339
|
+
def protocol_relative_url?(url)
|
340
|
+
url.start_with?("//")
|
341
|
+
end
|
342
|
+
|
279
343
|
def proxy_uri
|
280
344
|
begin
|
281
345
|
if @options[:proxy]
|
282
|
-
proxy =
|
346
|
+
proxy = URI.parse(@options[:proxy])
|
283
347
|
else
|
284
|
-
proxy = ENV['http_proxy'] && ENV['http_proxy'] != "" ?
|
348
|
+
proxy = ENV['http_proxy'] && ENV['http_proxy'] != "" ? URI.parse(ENV['http_proxy']) : nil
|
285
349
|
end
|
286
|
-
rescue
|
350
|
+
rescue URI::InvalidURIError
|
287
351
|
proxy = nil
|
288
352
|
end
|
289
353
|
proxy
|
@@ -293,9 +357,9 @@ class FastImage
|
|
293
357
|
proxy = proxy_uri
|
294
358
|
|
295
359
|
if proxy
|
296
|
-
@http = Net::HTTP::Proxy(proxy.host, proxy.port).new(@parsed_uri.host, @parsed_uri.
|
360
|
+
@http = Net::HTTP::Proxy(proxy.host, proxy.port, proxy.user, proxy.password).new(@parsed_uri.host, @parsed_uri.port)
|
297
361
|
else
|
298
|
-
@http = Net::HTTP.new(@parsed_uri.host, @parsed_uri.
|
362
|
+
@http = Net::HTTP.new(@parsed_uri.host, @parsed_uri.port)
|
299
363
|
end
|
300
364
|
@http.use_ssl = (@parsed_uri.scheme == "https")
|
301
365
|
@http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
@@ -304,6 +368,7 @@ class FastImage
|
|
304
368
|
end
|
305
369
|
|
306
370
|
def fetch_using_read(readable)
|
371
|
+
readable.rewind if readable.respond_to?(:rewind)
|
307
372
|
# Pathnames respond to read, but always return the first
|
308
373
|
# chunk of the file unlike an IO (even though the
|
309
374
|
# docuementation for it refers to IO). Need to supply
|
@@ -328,6 +393,7 @@ class FastImage
|
|
328
393
|
end
|
329
394
|
|
330
395
|
def fetch_using_file_open
|
396
|
+
@content_length = File.size?(@uri)
|
331
397
|
File.open(@uri) do |s|
|
332
398
|
fetch_using_read(s)
|
333
399
|
end
|
@@ -338,7 +404,14 @@ class FastImage
|
|
338
404
|
|
339
405
|
begin
|
340
406
|
result = send("parse_#{@property}")
|
341
|
-
if result
|
407
|
+
if result != nil
|
408
|
+
# extract exif orientation if it was found
|
409
|
+
if @property == :size && result.size == 3
|
410
|
+
@orientation = result.pop
|
411
|
+
else
|
412
|
+
@orientation = 1
|
413
|
+
end
|
414
|
+
|
342
415
|
instance_variable_set("@#{@property}", result)
|
343
416
|
else
|
344
417
|
raise CannotParseImage
|
@@ -353,6 +426,21 @@ class FastImage
|
|
353
426
|
send("parse_size_for_#{@type}")
|
354
427
|
end
|
355
428
|
|
429
|
+
def parse_animated
|
430
|
+
@type = parse_type unless @type
|
431
|
+
@type == :gif ? send("parse_animated_for_#{@type}") : nil
|
432
|
+
end
|
433
|
+
|
434
|
+
def fetch_using_base64(uri)
|
435
|
+
decoded = begin
|
436
|
+
Base64.decode64(uri.split(',')[1])
|
437
|
+
rescue
|
438
|
+
raise CannotParseImage
|
439
|
+
end
|
440
|
+
@content_length = decoded.size
|
441
|
+
fetch_using_read StringIO.new(decoded)
|
442
|
+
end
|
443
|
+
|
356
444
|
module StreamUtil # :nodoc:
|
357
445
|
def read_byte
|
358
446
|
read(1)[0].ord
|
@@ -382,20 +470,22 @@ class FastImage
|
|
382
470
|
@str = ''
|
383
471
|
end
|
384
472
|
|
473
|
+
# Peeking beyond the end of the input will raise
|
385
474
|
def peek(n)
|
386
|
-
while @strpos + n
|
475
|
+
while @strpos + n > @str.size
|
387
476
|
unused_str = @str[@strpos..-1]
|
477
|
+
|
388
478
|
new_string = @read_fiber.resume
|
479
|
+
new_string = @read_fiber.resume if new_string.is_a? Net::ReadAdapter
|
389
480
|
raise CannotParseImage if !new_string
|
390
|
-
|
391
481
|
# we are dealing with bytes here, so force the encoding
|
392
|
-
new_string.force_encoding("ASCII-8BIT") if
|
482
|
+
new_string.force_encoding("ASCII-8BIT") if new_string.respond_to? :force_encoding
|
393
483
|
|
394
484
|
@str = unused_str + new_string
|
395
485
|
@strpos = 0
|
396
486
|
end
|
397
487
|
|
398
|
-
|
488
|
+
@str[@strpos, n]
|
399
489
|
end
|
400
490
|
|
401
491
|
def read(n)
|
@@ -404,6 +494,24 @@ class FastImage
|
|
404
494
|
@pos += n
|
405
495
|
result
|
406
496
|
end
|
497
|
+
|
498
|
+
def skip(n)
|
499
|
+
discarded = 0
|
500
|
+
fetched = @str[@strpos..-1].size
|
501
|
+
while n > fetched
|
502
|
+
discarded += @str[@strpos..-1].size
|
503
|
+
new_string = @read_fiber.resume
|
504
|
+
raise CannotParseImage if !new_string
|
505
|
+
|
506
|
+
new_string.force_encoding("ASCII-8BIT") if new_string.respond_to? :force_encoding
|
507
|
+
|
508
|
+
fetched += new_string.size
|
509
|
+
@str = new_string
|
510
|
+
@strpos = 0
|
511
|
+
end
|
512
|
+
@strpos = @strpos + n - discarded
|
513
|
+
@pos += n
|
514
|
+
end
|
407
515
|
end
|
408
516
|
|
409
517
|
class IOStream < SimpleDelegator # :nodoc:
|
@@ -411,7 +519,7 @@ class FastImage
|
|
411
519
|
end
|
412
520
|
|
413
521
|
def parse_type
|
414
|
-
case @stream.peek(2)
|
522
|
+
parsed_type = case @stream.peek(2)
|
415
523
|
when "BM"
|
416
524
|
:bmp
|
417
525
|
when "GI"
|
@@ -421,7 +529,12 @@ class FastImage
|
|
421
529
|
when 0x89.chr + "P"
|
422
530
|
:png
|
423
531
|
when "II", "MM"
|
424
|
-
|
532
|
+
case @stream.peek(11)[8..10]
|
533
|
+
when "APC", "CR\002"
|
534
|
+
nil # do not recognise CRW or CR2 as tiff
|
535
|
+
else
|
536
|
+
:tiff
|
537
|
+
end
|
425
538
|
when '8B'
|
426
539
|
:psd
|
427
540
|
when "\0\0"
|
@@ -431,31 +544,100 @@ class FastImage
|
|
431
544
|
when 2 then :cur
|
432
545
|
end
|
433
546
|
when "RI"
|
434
|
-
if @stream.peek(12)[8..11] == "WEBP"
|
435
|
-
:webp
|
436
|
-
else
|
437
|
-
raise UnknownImageType
|
438
|
-
end
|
547
|
+
:webp if @stream.peek(12)[8..11] == "WEBP"
|
439
548
|
when "<s"
|
440
|
-
:svg
|
441
|
-
when
|
442
|
-
if
|
443
|
-
|
444
|
-
|
445
|
-
|
549
|
+
:svg if @stream.peek(4) == "<svg"
|
550
|
+
when /\s\s|\s<|<[?!]/
|
551
|
+
# Peek 10 more chars each time, and if end of file is reached just raise
|
552
|
+
# unknown. We assume the <svg tag cannot be within 10 chars of the end of
|
553
|
+
# the file, and is within the first 250 chars.
|
554
|
+
begin
|
555
|
+
:svg if (1..25).detect {|n| @stream.peek(10 * n).include?("<svg")}
|
556
|
+
rescue FiberError
|
557
|
+
nil
|
446
558
|
end
|
447
|
-
else
|
448
|
-
raise UnknownImageType
|
449
559
|
end
|
560
|
+
|
561
|
+
parsed_type or raise UnknownImageType
|
450
562
|
end
|
451
563
|
|
452
564
|
def parse_size_for_ico
|
453
|
-
@stream.read(
|
565
|
+
icons = @stream.read(6)[4..5].unpack('v').first
|
566
|
+
sizes = icons.times.map { @stream.read(16).unpack('C2').map { |x| x == 0 ? 256 : x } }.sort_by { |w,h| w * h }
|
567
|
+
sizes.last
|
454
568
|
end
|
455
569
|
alias_method :parse_size_for_cur, :parse_size_for_ico
|
456
570
|
|
571
|
+
class Gif # :nodoc:
|
572
|
+
def initialize(stream)
|
573
|
+
@stream = stream
|
574
|
+
end
|
575
|
+
|
576
|
+
def width_and_height
|
577
|
+
@stream.read(11)[6..10].unpack('SS')
|
578
|
+
end
|
579
|
+
|
580
|
+
# Checks if a delay between frames exists and if it does, then the GIFs is
|
581
|
+
# animated
|
582
|
+
def animated?
|
583
|
+
frames = 0
|
584
|
+
|
585
|
+
# "GIF" + version (3) + width (2) + height (2)
|
586
|
+
@stream.skip(10)
|
587
|
+
|
588
|
+
# fields (1) + bg color (1) + pixel ratio (1)
|
589
|
+
fields = @stream.read(3).unpack("CCC")[0]
|
590
|
+
if fields & 0x80 != 0 # Global Color Table
|
591
|
+
# 2 * (depth + 1) colors, each occupying 3 bytes (RGB)
|
592
|
+
@stream.skip(3 * 2 ** ((fields & 0x7) + 1))
|
593
|
+
end
|
594
|
+
|
595
|
+
loop do
|
596
|
+
block_type = @stream.read(1).unpack("C")[0]
|
597
|
+
|
598
|
+
if block_type == 0x21 # Graphic Control Extension
|
599
|
+
# extension type (1) + size (1)
|
600
|
+
size = @stream.read(2).unpack("CC")[1]
|
601
|
+
@stream.skip(size)
|
602
|
+
skip_sub_blocks
|
603
|
+
elsif block_type == 0x2C # Image Descriptor
|
604
|
+
frames += 1
|
605
|
+
return true if frames > 1
|
606
|
+
|
607
|
+
# left position (2) + top position (2) + width (2) + height (2) + fields (1)
|
608
|
+
fields = @stream.read(9).unpack("SSSSC")[4]
|
609
|
+
if fields & 0x80 != 0 # Local Color Table
|
610
|
+
# 2 * (depth + 1) colors, each occupying 3 bytes (RGB)
|
611
|
+
@stream.skip(3 * 2 ** ((fields & 0x7) + 1))
|
612
|
+
end
|
613
|
+
|
614
|
+
@stream.skip(1) # LZW min code size (1)
|
615
|
+
skip_sub_blocks
|
616
|
+
else
|
617
|
+
break # unrecognized block
|
618
|
+
end
|
619
|
+
end
|
620
|
+
|
621
|
+
false
|
622
|
+
end
|
623
|
+
|
624
|
+
private
|
625
|
+
|
626
|
+
def skip_sub_blocks
|
627
|
+
loop do
|
628
|
+
size = @stream.read(1).unpack("C")[0]
|
629
|
+
if size == 0
|
630
|
+
break
|
631
|
+
else
|
632
|
+
@stream.skip(size)
|
633
|
+
end
|
634
|
+
end
|
635
|
+
end
|
636
|
+
end
|
637
|
+
|
457
638
|
def parse_size_for_gif
|
458
|
-
@stream
|
639
|
+
gif = Gif.new(@stream)
|
640
|
+
gif.width_and_height
|
459
641
|
end
|
460
642
|
|
461
643
|
def parse_size_for_png
|
@@ -463,10 +645,11 @@ class FastImage
|
|
463
645
|
end
|
464
646
|
|
465
647
|
def parse_size_for_jpeg
|
648
|
+
exif = nil
|
466
649
|
loop do
|
467
650
|
@state = case @state
|
468
651
|
when nil
|
469
|
-
@stream.
|
652
|
+
@stream.skip(2)
|
470
653
|
:started
|
471
654
|
when :started
|
472
655
|
@stream.read_byte == 0xFF ? :sof : :started
|
@@ -478,7 +661,8 @@ class FastImage
|
|
478
661
|
io = StringIO.new(data)
|
479
662
|
if io.read(4) == "Exif"
|
480
663
|
io.read(2)
|
481
|
-
|
664
|
+
new_exif = Exif.new(IOStream.new(io)) rescue nil
|
665
|
+
exif ||= new_exif # only use the first APP1 segment
|
482
666
|
end
|
483
667
|
:started
|
484
668
|
when 0xe0..0xef
|
@@ -492,14 +676,14 @@ class FastImage
|
|
492
676
|
end
|
493
677
|
when :skipframe
|
494
678
|
skip_chars = @stream.read_int - 2
|
495
|
-
@stream.
|
679
|
+
@stream.skip(skip_chars)
|
496
680
|
:started
|
497
681
|
when :readsize
|
498
|
-
|
682
|
+
@stream.skip(3)
|
499
683
|
height = @stream.read_int
|
500
684
|
width = @stream.read_int
|
501
|
-
width, height = height, width if
|
502
|
-
return [width, height]
|
685
|
+
width, height = height, width if exif && exif.rotated?
|
686
|
+
return [width, height, exif ? exif.orientation : 1]
|
503
687
|
end
|
504
688
|
end
|
505
689
|
end
|
@@ -508,10 +692,10 @@ class FastImage
|
|
508
692
|
d = @stream.read(32)[14..28]
|
509
693
|
header = d.unpack("C")[0]
|
510
694
|
|
511
|
-
result = if header ==
|
512
|
-
d[4..-1].unpack('l<l<')
|
513
|
-
else
|
695
|
+
result = if header == 12
|
514
696
|
d[4..8].unpack('SS')
|
697
|
+
else
|
698
|
+
d[4..-1].unpack('l<l<')
|
515
699
|
end
|
516
700
|
|
517
701
|
# ImageHeight is expressed in pixels. The absolute value is necessary because ImageHeight can be negative
|
@@ -520,7 +704,7 @@ class FastImage
|
|
520
704
|
|
521
705
|
def parse_size_for_webp
|
522
706
|
vp8 = @stream.read(16)[12..15]
|
523
|
-
|
707
|
+
_len = @stream.read(4).unpack("V")
|
524
708
|
case vp8
|
525
709
|
when "VP8 "
|
526
710
|
parse_size_vp8
|
@@ -539,7 +723,7 @@ class FastImage
|
|
539
723
|
end
|
540
724
|
|
541
725
|
def parse_size_vp8l
|
542
|
-
@stream.
|
726
|
+
@stream.skip(1) # 0x2f
|
543
727
|
b1, b2, b3, b4 = @stream.read(4).bytes.to_a
|
544
728
|
[1 + (((b2 & 0x3f) << 8) | b1), 1 + (((b4 & 0xF) << 10) | (b3 << 2) | ((b2 & 0xC0) >> 6))]
|
545
729
|
end
|
@@ -558,14 +742,16 @@ class FastImage
|
|
558
742
|
end
|
559
743
|
|
560
744
|
class Exif # :nodoc:
|
561
|
-
attr_reader :width, :height
|
745
|
+
attr_reader :width, :height, :orientation
|
746
|
+
|
562
747
|
def initialize(stream)
|
563
748
|
@stream = stream
|
749
|
+
@width, @height, @orientation = nil
|
564
750
|
parse_exif
|
565
751
|
end
|
566
752
|
|
567
753
|
def rotated?
|
568
|
-
@orientation
|
754
|
+
@orientation >= 5
|
569
755
|
end
|
570
756
|
|
571
757
|
private
|
@@ -611,9 +797,15 @@ class FastImage
|
|
611
797
|
@stream.read(2) # 42
|
612
798
|
|
613
799
|
offset = @stream.read(4).unpack(@long)[0]
|
614
|
-
@stream.
|
800
|
+
if @stream.respond_to?(:skip)
|
801
|
+
@stream.skip(offset - 8)
|
802
|
+
else
|
803
|
+
@stream.read(offset - 8)
|
804
|
+
end
|
615
805
|
|
616
806
|
parse_exif_ifd
|
807
|
+
|
808
|
+
@orientation ||= 1
|
617
809
|
end
|
618
810
|
|
619
811
|
end
|
@@ -621,9 +813,9 @@ class FastImage
|
|
621
813
|
def parse_size_for_tiff
|
622
814
|
exif = Exif.new(@stream)
|
623
815
|
if exif.rotated?
|
624
|
-
[exif.height, exif.width]
|
816
|
+
[exif.height, exif.width, exif.orientation]
|
625
817
|
else
|
626
|
-
[exif.width, exif.height]
|
818
|
+
[exif.width, exif.height, exif.orientation]
|
627
819
|
end
|
628
820
|
end
|
629
821
|
|
@@ -634,6 +826,7 @@ class FastImage
|
|
634
826
|
class Svg # :nodoc:
|
635
827
|
def initialize(stream)
|
636
828
|
@stream = stream
|
829
|
+
@width, @height, @ratio, @viewbox_width, @viewbox_height = nil
|
637
830
|
parse_svg
|
638
831
|
end
|
639
832
|
|
@@ -644,6 +837,10 @@ class FastImage
|
|
644
837
|
[@width, @width / @ratio]
|
645
838
|
elsif @height && @ratio
|
646
839
|
[@height * @ratio, @height]
|
840
|
+
elsif @viewbox_width && @viewbox_height
|
841
|
+
[@viewbox_width, @viewbox_height]
|
842
|
+
else
|
843
|
+
nil
|
647
844
|
end
|
648
845
|
end
|
649
846
|
|
@@ -666,14 +863,20 @@ class FastImage
|
|
666
863
|
return if @width
|
667
864
|
elsif attr_name.join =~ /viewbox/i
|
668
865
|
values = attr_value.split(/\s/)
|
669
|
-
|
866
|
+
if values[2].to_f > 0 && values[3].to_f > 0
|
867
|
+
@ratio = values[2].to_f / values[3].to_f
|
868
|
+
@viewbox_width = values[2].to_i
|
869
|
+
@viewbox_height = values[3].to_i
|
870
|
+
end
|
670
871
|
end
|
671
872
|
when /\w/
|
672
873
|
attr_name << char
|
874
|
+
when "<"
|
875
|
+
attr_name = [char]
|
673
876
|
when ">"
|
674
877
|
state = :stop if state == :started
|
675
878
|
else
|
676
|
-
state = :started if attr_name.join == "svg"
|
879
|
+
state = :started if attr_name.join == "<svg"
|
677
880
|
attr_name.clear
|
678
881
|
end
|
679
882
|
end
|
@@ -694,4 +897,9 @@ class FastImage
|
|
694
897
|
svg = Svg.new(@stream)
|
695
898
|
svg.width_and_height
|
696
899
|
end
|
900
|
+
|
901
|
+
def parse_animated_for_gif
|
902
|
+
gif = Gif.new(@stream)
|
903
|
+
gif.animated?
|
904
|
+
end
|
697
905
|
end
|