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 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