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