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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 14c971688e6b8e577baf835c98f64700081af0bd
4
- data.tar.gz: 25ffa6572caf216c99a7dcfbdc2a1d6cc5a40b2e
2
+ SHA256:
3
+ metadata.gz: 4124655c4f8af69e9b8bf9bfa433a11161e9ea97c23e755a196555012eadf75b
4
+ data.tar.gz: 40fef6aa908fc14799f2cb53e80d5081bef11ce0482d86c5947841fb29ee9604
5
5
  SHA512:
6
- metadata.gz: 7ae55b045d02d4c1a7ce3452ed3da248e01312a9059bbef29c6e57a780a53ba0874c1566b9703b0ddb143e12d7827609194fa52b151cbe5a30861c2e6644c685
7
- data.tar.gz: 15c0ee070dc9f516592445a29ae66c2c38f5b93a1572c5befa68a3d9227806a7345840ed8a88c1352b8a307ed0fa7b6224efa0d19d219f92d708863347fffda8
6
+ metadata.gz: 205fb43dacd3239f8425dd8cbd311be8c375f3c377cbf71771b87304b0c62780905a366783b1b5606e7386edf9ed2fe1cab138b8a20ad47f238150d5143b93db
7
+ data.tar.gz: 219ac67b55c23bb59458fc57d165a76c0bf5855c971c705d033103c01848cbb9f3fb4a1e8024419192ced93f076d223fb6e3ce77289b8ed94858d0e58c5387df
data/README.textile CHANGED
@@ -1,4 +1,5 @@
1
- !https://travis-ci.org/sdsykes/fastimage.png?branch=master!:https://travis-ci.org/sdsykes/fastimage
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
- Fastimage can also read local (and other) files - anything that is not parseable as a URI will be
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 :read - for
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 :proxy argument if you want to specify the proxy address in the call.
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 :timeout => number_of_seconds.
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 will nil if it encounters an error, but you can pass :raise_on_failure => true to get an exception.
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 :http_header argument to specify headers, e.g., :http_header => {'User-Agent' => 'Fake Browser'}.
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. But take care to sanitise the strings passed to FastImage; it will try to read from whatever is passed.
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[:type_only] ? :type : :size
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 = Addressable::URI.parse(uri)
185
- rescue Addressable::URI::InvalidURIError
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
- raise ImageFetchFailure if @options[:raise_on_failure]
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 UnknownImageType if @options[:raise_on_failure]
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
- newly_parsed_uri = Addressable::URI.parse(res['Location'])
235
- # The new location may be relative - check for that
236
- if newly_parsed_uri.scheme != "http" && newly_parsed_uri.scheme != "https"
237
- @parsed_uri.path = res['Location']
238
- else
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 = Addressable::URI.parse(@options[:proxy])
346
+ proxy = URI.parse(@options[:proxy])
283
347
  else
284
- proxy = ENV['http_proxy'] && ENV['http_proxy'] != "" ? Addressable::URI.parse(ENV['http_proxy']) : nil
348
+ proxy = ENV['http_proxy'] && ENV['http_proxy'] != "" ? URI.parse(ENV['http_proxy']) : nil
285
349
  end
286
- rescue Addressable::URI::InvalidURIError
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.inferred_port)
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.inferred_port)
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 - 1 >= @str.size
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 String.method_defined? :force_encoding
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
- result = @str[@strpos..(@strpos + n - 1)]
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
- :tiff
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 @stream.peek(100).include?("<svg")
443
- :svg
444
- else
445
- raise UnknownImageType
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(8)[6..7].unpack('CC').map{|byte| byte == 0 ? 256 : byte }
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.read(11)[6..10].unpack('SS')
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.read(2)
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
- @exif = Exif.new(IOStream.new(io)) rescue nil
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.read(skip_chars)
679
+ @stream.skip(skip_chars)
496
680
  :started
497
681
  when :readsize
498
- s = @stream.read(3)
682
+ @stream.skip(3)
499
683
  height = @stream.read_int
500
684
  width = @stream.read_int
501
- width, height = height, width if @exif && @exif.rotated?
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 == 40
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
- len = @stream.read(4).unpack("V")
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.read(1) # 0x2f
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 && @orientation >= 5
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.read(offset - 8)
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
- @ratio = values[2].to_f / values[3].to_f
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