fastimage_discourse 1.6.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: eede86e80e904f2cbcb1535f1654368e6b48d1da
4
+ data.tar.gz: 14727e72c17b02db76d392306b990ef0919b2bcc
5
+ SHA512:
6
+ metadata.gz: fdf18044ef2a50cccd1e3c985834db078587293f2993acede3c1a8e968a19f5d61dc863618a78d1fb58637089eb6e26bad765b32a8f2f3166fedcfa72f0ad7db
7
+ data.tar.gz: fcb48ff91c85b42bc43f4dafb96843dea8c4fd220a1450691be7f8aa68cd936c2041aff6797dceb5945a131a427470d305605b7f5cd81b9c0556b6dc009aaa9d
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008-2013 Stephen Sykes
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,155 @@
1
+ !https://travis-ci.org/sdsykes/fastimage.png?branch=master!:https://travis-ci.org/sdsykes/fastimage
2
+
3
+ h1. FastImage
4
+
5
+ h4. FastImage finds the size or type of an image given its uri by fetching as little as needed
6
+
7
+ h2. The problem
8
+
9
+ Your app needs to find the size or type of an image. This could be for adding width and height attributes to an image tag, for adjusting layouts or overlays to fit an image or any other of dozens of reasons.
10
+
11
+ But the image is not locally stored - it's on another asset server, or in the cloud - at Amazon S3 for example.
12
+
13
+ You don't want to download the entire image to your app server - it could be many tens of kilobytes, or even megabytes just to get this information. For most common image types (GIF, PNG, BMP etc.), the size of the image is simply stored at the start of the file. For JPEG files it's a little bit more complex, but even so you do not need to fetch much of the image to find the size.
14
+
15
+ FastImage does this minimal fetch for image types GIF, JPEG, PNG, TIFF, BMP, ICO, CUR, PSD and WEBP. And it doesn't rely on installing external libraries such as RMagick (which relies on ImageMagick or GraphicsMagick) or ImageScience (which relies on FreeImage).
16
+
17
+ You only need supply the uri, and FastImage will do the rest.
18
+
19
+ h2. Features
20
+
21
+ Fastimage can also read local (and other) files, and uses the open-uri library to do so.
22
+
23
+ FastImage will automatically read from any object that responds to :read - for
24
+ instance an IO object if that is passed instead of a URI.
25
+
26
+ FastImage will follow up to 4 HTTP redirects to get the image.
27
+
28
+ FastImage will obey the http_proxy setting in your environment to route requests via a proxy.
29
+
30
+ You can add a timeout to the request which will limit the request time by passing :timeout => number_of_seconds.
31
+
32
+ FastImage normally replies will nil if it encounters an error, but you can pass :raise_on_failure => true to get an exception.
33
+
34
+ You may optionally use the `addressable` gem for URI parsing or a custom URI parser.
35
+
36
+ To use the `addressable` gem for uri parsing.
37
+
38
+ <pre lang="ruby"><code>
39
+ require 'addressable/uri'
40
+ FastImage.use_addressable_uri_parser
41
+ </code></pre>
42
+
43
+ To use a custom URI parser like, for example, the old URI parser in Ruby 2.2 for example use:
44
+
45
+ <pre lang="ruby"><code>
46
+ FastImage.uri_parser = URI::RFC2396_Parser.new
47
+ </code></pre>
48
+
49
+ h2. Examples
50
+
51
+ <pre lang="ruby"><code>
52
+ require 'fastimage'
53
+
54
+ FastImage.size("http://stephensykes.com/images/ss.com_x.gif")
55
+ => [266, 56] # width, height
56
+ FastImage.type("http://stephensykes.com/images/pngimage")
57
+ => :png
58
+ FastImage.type("/some/local/file.gif")
59
+ => :gif
60
+ FastImage.size("http://upload.wikimedia.org/wikipedia/commons/b/b4/Mardin_1350660_1350692_33_images.jpg", :raise_on_failure=>true, :timeout=>0.1)
61
+ => FastImage::ImageFetchFailure: FastImage::ImageFetchFailure
62
+ FastImage.size("http://upload.wikimedia.org/wikipedia/commons/b/b4/Mardin_1350660_1350692_33_images.jpg", :raise_on_failure=>true, :timeout=>2.0)
63
+ => [9545, 6623]
64
+ </code></pre>
65
+
66
+ h2. Installation
67
+
68
+ h4. Gem
69
+
70
+ bc. gem install fastimage
71
+
72
+ h4. Rails
73
+
74
+ Add fastimage to your Gemfile, and bundle.
75
+
76
+ Then you're off - just use @FastImage.size()@ and @FastImage.type()@ in your code as in the examples.
77
+
78
+ h2. Documentation
79
+
80
+ "http://sdsykes.github.io/fastimage/rdoc/FastImage.html":http://sdsykes.github.io/fastimage/rdoc/FastImage.html
81
+
82
+ h2. Benchmark
83
+
84
+ It's way faster than conventional methods (for example the image_size gem) for most types of file when fetching over the wire.
85
+
86
+ <pre lang="ruby"><code>
87
+ irb> uri = "http://upload.wikimedia.org/wikipedia/commons/b/b4/Mardin_1350660_1350692_33_images.jpg"
88
+ irb> puts Benchmark.measure {open(uri, 'rb') {|fh| p ImageSize.new(fh).size}}
89
+ [9545, 6623]
90
+ 0.680000 0.250000 0.930000 ( 7.571887)
91
+
92
+ irb> puts Benchmark.measure {p FastImage.size(uri)}
93
+ [9545, 6623]
94
+ 0.010000 0.000000 0.010000 ( 0.090640)
95
+ </code></pre>
96
+
97
+ The file is fetched in about 7.5 seconds in this test (the number in brackets is the total time taken), but as FastImage doesn't need to fetch the whole thing, it completes in less than 0.1s.
98
+
99
+ You'll see similar excellent results for the other file types, except for TIFF. Unfortunately TIFFs tend to have their
100
+ metadata towards the end of the file, so it makes little difference to do a minimal fetch. The result shown below is
101
+ mostly dependent on the exact internet conditions during the test, and little to do with the library used.
102
+
103
+ <pre lang="ruby"><code>
104
+ irb> uri = "http://upload.wikimedia.org/wikipedia/commons/1/11/Shinbutsureijoushuincho.tiff"
105
+ irb> puts Benchmark.measure {open(uri, 'rb') {|fh| p ImageSize.new(fh).size}}
106
+ [1120, 1559]
107
+ 1.080000 0.370000 1.450000 ( 13.766962)
108
+
109
+ irb> puts Benchmark.measure {p FastImage.size(uri)}
110
+ [1120, 1559]
111
+ 3.490000 3.810000 7.300000 ( 11.754315)
112
+ </code></pre>
113
+
114
+ h2. Tests
115
+
116
+ You'll need to @gem install fakeweb@ and possibly also @gem install test-unit@ to be able to run the tests.
117
+
118
+ bc.. $ ruby test.rb
119
+ Run options:
120
+
121
+ # Running tests:
122
+
123
+ Finished tests in 1.033640s, 23.2189 tests/s, 82.2337 assertions/s.
124
+ 24 tests, 85 assertions, 0 failures, 0 errors, 0 skips
125
+
126
+ h2. References
127
+
128
+ * "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
+ * "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
+ * "imagesize gem":https://rubygems.org/gems/imagesize
132
+ * "EXIF Reader":https://github.com/remvee/exifr
133
+
134
+ h2. FastImage in other languages
135
+
136
+ * "Python by bmuller":https://github.com/bmuller/fastimage
137
+ * "Swift by kaishin":https://github.com/kaishin/ImageScout
138
+ * "Go by rubenfonseca":https://github.com/rubenfonseca/fastimage
139
+
140
+ h2. Licence
141
+
142
+ MIT, see file "MIT-LICENSE":MIT-LICENSE
143
+
144
+ h2. Contributors
145
+
146
+ Pull requests and suggestions are always welcome. Thanks to all the contributors!
147
+
148
+ * @felixbuenemann
149
+ * @speedmax
150
+ * @sebastianludwig
151
+ * @benjaminjackson
152
+ * @muffinista
153
+ * @marcandre
154
+ * @apanzerj
155
+
@@ -0,0 +1,619 @@
1
+ # coding: ASCII-8BIT
2
+
3
+ # FastImage finds the size or type of an image given its uri.
4
+ # It is careful to only fetch and parse as much of the image as is needed to determine the result.
5
+ # It does this by using a feature of Net::HTTP that yields strings from the resource being fetched
6
+ # as soon as the packets arrive.
7
+ #
8
+ # No external libraries such as ImageMagick are used here, this is a very lightweight solution to
9
+ # finding image information.
10
+ #
11
+ # FastImage knows about GIF, JPEG, BMP, TIFF, ICO, CUR, PNG, PSD and WEBP files.
12
+ #
13
+ # FastImage can also read files from the local filesystem by supplying the path instead of a uri.
14
+ # In this case FastImage uses the open-uri library to read the file in chunks of 256 bytes until
15
+ # it has enough. This is possibly a useful bandwidth-saving feature if the file is on a network
16
+ # attached disk rather than truly local.
17
+ #
18
+ # FastImage will automatically read from any object that responds to :read - for
19
+ # instance an IO object if that is passed instead of a URI.
20
+ #
21
+ # FastImage will follow up to 4 HTTP redirects to get the image.
22
+ #
23
+ # === Examples
24
+ # require 'fastimage'
25
+ #
26
+ # FastImage.size("http://stephensykes.com/images/ss.com_x.gif")
27
+ # => [266, 56]
28
+ # FastImage.type("http://stephensykes.com/images/pngimage")
29
+ # => :png
30
+ # FastImage.type("/some/local/file.gif")
31
+ # => :gif
32
+ # File.open("/some/local/file.gif", "r") {|io| FastImage.type(io)}
33
+ # => :gif
34
+ #
35
+ # === References
36
+ # * http://snippets.dzone.com/posts/show/805
37
+ # * http://www.anttikupila.com/flash/getting-jpg-dimensions-with-as3-without-loading-the-entire-file/
38
+ # * http://pennysmalls.wordpress.com/2008/08/19/find-jpeg-dimensions-fast-in-pure-ruby-no-ima/
39
+ # * https://rubygems.org/gems/imagesize
40
+ # * https://github.com/remvee/exifr
41
+ #
42
+
43
+ require 'net/https'
44
+ require 'fastimage/fbr.rb'
45
+ require 'open-uri'
46
+ require 'delegate'
47
+ require 'pathname'
48
+ require 'zlib'
49
+
50
+ class FastImage
51
+ attr_reader :size, :type
52
+
53
+ attr_reader :bytes_read
54
+
55
+ class FastImageException < StandardError # :nodoc:
56
+ end
57
+ class UnknownImageType < FastImageException # :nodoc:
58
+ end
59
+ class ImageFetchFailure < FastImageException # :nodoc:
60
+ end
61
+ class SizeNotFound < FastImageException # :nodoc:
62
+ end
63
+ class CannotParseImage < FastImageException # :nodoc:
64
+ end
65
+
66
+ DefaultTimeout = 2 unless const_defined?(:DefaultTimeout)
67
+
68
+ LocalFileChunkSize = 256 unless const_defined?(:LocalFileChunkSize)
69
+
70
+ # Parser object should respond to #parse
71
+ # and raise a URI::InvalidURIError if something goes wrong
72
+ def self.uri_parser=(parser)
73
+ @uri_parser = parser
74
+ end
75
+
76
+ # Helper that sets URI parsing to use the Addressable gem
77
+ def self.use_addressable_uri_parser
78
+ require 'addressable/uri'
79
+ self.uri_parser = Class.new do
80
+ def self.parse(location)
81
+ Addressable::URI.parse(location)
82
+ rescue Addressable::URI::InvalidURIError
83
+ raise URI::InvalidURIError
84
+ end
85
+ end
86
+ end
87
+
88
+ def self.parse_uri(location)
89
+ (@uri_parser || URI).parse(location)
90
+ end
91
+
92
+ # Returns an array containing the width and height of the image.
93
+ # It will return nil if the image could not be fetched, or if the image type was not recognised.
94
+ #
95
+ # By default there is a timeout of 2 seconds for opening and reading from a remote server.
96
+ # This can be changed by passing a :timeout => number_of_seconds in the options.
97
+ #
98
+ # If you wish FastImage to raise if it cannot size the image for any reason, then pass
99
+ # :raise_on_failure => true in the options.
100
+ #
101
+ # FastImage knows about GIF, JPEG, BMP, TIFF, ICO, CUR, PNG, PSD and WEBP files.
102
+ #
103
+ # === Example
104
+ #
105
+ # require 'fastimage'
106
+ #
107
+ # FastImage.size("http://stephensykes.com/images/ss.com_x.gif")
108
+ # => [266, 56]
109
+ # FastImage.size("http://stephensykes.com/images/pngimage")
110
+ # => [16, 16]
111
+ # FastImage.size("http://farm4.static.flickr.com/3023/3047236863_9dce98b836.jpg")
112
+ # => [500, 375]
113
+ # FastImage.size("http://www-ece.rice.edu/~wakin/images/lena512.bmp")
114
+ # => [512, 512]
115
+ # FastImage.size("test/fixtures/test.jpg")
116
+ # => [882, 470]
117
+ # FastImage.size("http://pennysmalls.com/does_not_exist")
118
+ # => nil
119
+ # FastImage.size("http://pennysmalls.com/does_not_exist", :raise_on_failure=>true)
120
+ # => raises FastImage::ImageFetchFailure
121
+ # FastImage.size("http://stephensykes.com/favicon.ico", :raise_on_failure=>true)
122
+ # => [16, 16]
123
+ # FastImage.size("http://stephensykes.com/images/squareBlue.icns", :raise_on_failure=>true)
124
+ # => raises FastImage::UnknownImageType
125
+ # FastImage.size("http://stephensykes.com/favicon.ico", :raise_on_failure=>true, :timeout=>0.01)
126
+ # => raises FastImage::ImageFetchFailure
127
+ # FastImage.size("http://stephensykes.com/images/faulty.jpg", :raise_on_failure=>true)
128
+ # => raises FastImage::SizeNotFound
129
+ #
130
+ # === Supported options
131
+ # [:timeout]
132
+ # Overrides the default timeout of 2 seconds. Applies both to reading from and opening the http connection.
133
+ # [:raise_on_failure]
134
+ # If set to true causes an exception to be raised if the image size cannot be found for any reason.
135
+ #
136
+ def self.size(uri, options={})
137
+ new(uri, options).size
138
+ end
139
+
140
+ # Returns an symbol indicating the image type fetched from a uri.
141
+ # It will return nil if the image could not be fetched, or if the image type was not recognised.
142
+ #
143
+ # By default there is a timeout of 2 seconds for opening and reading from a remote server.
144
+ # This can be changed by passing a :timeout => number_of_seconds in the options.
145
+ #
146
+ # If you wish FastImage to raise if it cannot find the type of the image for any reason, then pass
147
+ # :raise_on_failure => true in the options.
148
+ #
149
+ # === Example
150
+ #
151
+ # require 'fastimage'
152
+ #
153
+ # FastImage.type("http://stephensykes.com/images/ss.com_x.gif")
154
+ # => :gif
155
+ # FastImage.type("http://stephensykes.com/images/pngimage")
156
+ # => :png
157
+ # FastImage.type("http://farm4.static.flickr.com/3023/3047236863_9dce98b836.jpg")
158
+ # => :jpeg
159
+ # FastImage.type("http://www-ece.rice.edu/~wakin/images/lena512.bmp")
160
+ # => :bmp
161
+ # FastImage.type("test/fixtures/test.jpg")
162
+ # => :jpeg
163
+ # FastImage.type("http://stephensykes.com/does_not_exist")
164
+ # => nil
165
+ # File.open("/some/local/file.gif", "r") {|io| FastImage.type(io)}
166
+ # => :gif
167
+ # FastImage.type("test/fixtures/test.tiff")
168
+ # => :tiff
169
+ # FastImage.type("test/fixtures/test.psd")
170
+ # => :psd
171
+ #
172
+ # === Supported options
173
+ # [:timeout]
174
+ # Overrides the default timeout of 2 seconds. Applies both to reading from and opening the http connection.
175
+ # [:raise_on_failure]
176
+ # If set to true causes an exception to be raised if the image type cannot be found for any reason.
177
+ #
178
+ def self.type(uri, options={})
179
+ new(uri, options.merge(:type_only=>true)).type
180
+ end
181
+
182
+ def initialize(uri, options={})
183
+ @property = options[:type_only] ? :type : :size
184
+ @timeout = options[:timeout] || DefaultTimeout
185
+ @uri = uri
186
+
187
+ if uri.respond_to?(:read)
188
+ fetch_using_read(uri)
189
+ else
190
+ begin
191
+ @parsed_uri = self.class.parse_uri(uri)
192
+ rescue URI::InvalidURIError
193
+ fetch_using_open_uri
194
+ else
195
+ if @parsed_uri.scheme == "http" || @parsed_uri.scheme == "https"
196
+ fetch_using_http
197
+ else
198
+ fetch_using_open_uri
199
+ end
200
+ end
201
+ end
202
+
203
+ uri.rewind if uri.respond_to?(:rewind)
204
+
205
+ raise SizeNotFound if options[:raise_on_failure] && @property == :size && !@size
206
+
207
+ rescue Timeout::Error, SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ECONNRESET,
208
+ ImageFetchFailure, Net::HTTPBadResponse, EOFError, Errno::ENOENT
209
+ raise ImageFetchFailure if options[:raise_on_failure]
210
+ rescue NoMethodError # 1.8.7p248 can raise this due to a net/http bug
211
+ raise ImageFetchFailure if options[:raise_on_failure]
212
+ rescue UnknownImageType
213
+ raise UnknownImageType if options[:raise_on_failure]
214
+ rescue CannotParseImage
215
+ if options[:raise_on_failure]
216
+ if @property == :size
217
+ raise SizeNotFound
218
+ else
219
+ raise ImageFetchFailure
220
+ end
221
+ end
222
+
223
+ end
224
+
225
+ private
226
+
227
+ def fetch_using_http
228
+ @redirect_count = 0
229
+
230
+ fetch_using_http_from_parsed_uri
231
+ end
232
+
233
+ def fetch_using_http_from_parsed_uri
234
+ setup_http
235
+ @http.request_get(@parsed_uri.request_uri, 'Accept-Encoding' => 'identity') do |res|
236
+ if res.is_a?(Net::HTTPRedirection) && @redirect_count < 4
237
+ @redirect_count += 1
238
+ begin
239
+ newly_parsed_uri = self.class.parse_uri(res['Location'])
240
+ # The new location may be relative - check for that
241
+ if newly_parsed_uri.scheme != "http" && newly_parsed_uri.scheme != "https"
242
+ @parsed_uri.path = res['Location']
243
+ else
244
+ @parsed_uri = newly_parsed_uri
245
+ end
246
+ rescue URI::InvalidURIError
247
+ else
248
+ fetch_using_http_from_parsed_uri
249
+ break
250
+ end
251
+ end
252
+
253
+ raise ImageFetchFailure unless res.is_a?(Net::HTTPSuccess)
254
+
255
+ read_fiber = Fiber.new do
256
+ res.read_body do |str|
257
+ Fiber.yield str
258
+ end
259
+ end
260
+
261
+ case res['content-encoding']
262
+ when 'deflate', 'gzip', 'x-gzip'
263
+ begin
264
+ gzip = Zlib::GzipReader.new(FiberStream.new(read_fiber))
265
+ rescue FiberError, Zlib::GzipFile::Error
266
+ raise CannotParseImage
267
+ end
268
+
269
+ read_fiber = Fiber.new do
270
+ while data = gzip.readline
271
+ Fiber.yield data
272
+ end
273
+ end
274
+ end
275
+
276
+ parse_packets FiberStream.new(read_fiber)
277
+
278
+ break # needed to actively quit out of the fetch
279
+ end
280
+ end
281
+
282
+ def proxy_uri
283
+ begin
284
+ proxy = ENV['http_proxy'] && ENV['http_proxy'] != "" ? self.class.parse_uri(ENV['http_proxy']) : nil
285
+ rescue URI::InvalidURIError
286
+ proxy = nil
287
+ end
288
+ proxy
289
+ end
290
+
291
+ def setup_http
292
+ proxy = proxy_uri
293
+
294
+ port = @parsed_uri.port
295
+ port ||= @parsed_uri.scheme == "https" ? 443 : 80
296
+
297
+ if proxy
298
+ @http = Net::HTTP::Proxy(proxy.host, proxy.port).new(@parsed_uri.host, port)
299
+ else
300
+ @http = Net::HTTP.new(@parsed_uri.host, port)
301
+ end
302
+ @http.use_ssl = (@parsed_uri.scheme == "https")
303
+ @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
304
+ @http.open_timeout = @timeout
305
+ @http.read_timeout = @timeout
306
+ end
307
+
308
+ def fetch_using_read(readable)
309
+ # Pathnames respond to read, but always return the first
310
+ # chunk of the file unlike an IO (even though the
311
+ # docuementation for it refers to IO). Need to supply
312
+ # an offset in this case.
313
+ if readable.is_a?(Pathname)
314
+ read_fiber = Fiber.new do
315
+ offset = 0
316
+ while str = readable.read(LocalFileChunkSize, offset)
317
+ Fiber.yield str
318
+ offset += LocalFileChunkSize
319
+ end
320
+ end
321
+ else
322
+ read_fiber = Fiber.new do
323
+ while str = readable.read(LocalFileChunkSize)
324
+ Fiber.yield str
325
+ end
326
+ end
327
+ end
328
+
329
+ parse_packets FiberStream.new(read_fiber)
330
+ end
331
+
332
+ def fetch_using_open_uri
333
+ open(@uri) do |s|
334
+ fetch_using_read(s)
335
+ end
336
+ end
337
+
338
+ def parse_packets(stream)
339
+ @stream = stream
340
+
341
+ begin
342
+ result = send("parse_#{@property}")
343
+ if result
344
+ instance_variable_set("@#{@property}", result)
345
+ else
346
+ raise CannotParseImage
347
+ end
348
+ rescue FiberError
349
+ raise CannotParseImage
350
+ end
351
+ end
352
+
353
+ def parse_size
354
+ @type = parse_type unless @type
355
+ send("parse_size_for_#{@type}")
356
+ end
357
+
358
+ module StreamUtil # :nodoc:
359
+ def read_byte
360
+ read(1)[0].ord
361
+ end
362
+
363
+ def read_int
364
+ read(2).unpack('n')[0]
365
+ end
366
+ end
367
+
368
+ class FiberStream # :nodoc:
369
+ include StreamUtil
370
+ attr_reader :pos
371
+
372
+ def initialize(read_fiber)
373
+ @read_fiber = read_fiber
374
+ @pos = 0
375
+ @strpos = 0
376
+ @str = ''
377
+ end
378
+
379
+ def peek(n)
380
+ while @strpos + n - 1 >= @str.size
381
+ unused_str = @str[@strpos..-1]
382
+ new_string = @read_fiber.resume
383
+ raise CannotParseImage if !new_string
384
+
385
+ # we are dealing with bytes here, so force the encoding
386
+ new_string.force_encoding("ASCII-8BIT") if String.method_defined? :force_encoding
387
+
388
+ @str = unused_str + new_string
389
+ @strpos = 0
390
+ end
391
+
392
+ @str[@strpos..(@strpos + n - 1)]
393
+ end
394
+
395
+ def read(n)
396
+ result = peek(n)
397
+ @strpos += n
398
+ @pos += n
399
+ result
400
+ end
401
+ end
402
+
403
+ class IOStream < SimpleDelegator # :nodoc:
404
+ include StreamUtil
405
+ end
406
+
407
+ def parse_type
408
+ case @stream.peek(2)
409
+ when "BM"
410
+ :bmp
411
+ when "GI"
412
+ :gif
413
+ when 0xff.chr + 0xd8.chr
414
+ :jpeg
415
+ when 0x89.chr + "P"
416
+ :png
417
+ when "II", "MM"
418
+ :tiff
419
+ when '8B'
420
+ :psd
421
+ when "\0\0"
422
+ # ico has either a 1 (for ico format) or 2 (for cursor) at offset 3
423
+ case @stream.peek(3).bytes.to_a.last
424
+ when 1 then :ico
425
+ when 2 then :cur
426
+ end
427
+ when "RI"
428
+ if @stream.peek(12)[8..11] == "WEBP"
429
+ :webp
430
+ else
431
+ raise UnknownImageType
432
+ end
433
+ else
434
+ raise UnknownImageType
435
+ end
436
+ end
437
+
438
+ def parse_size_for_ico
439
+ @stream.read(8)[6..7].unpack('CC').map{|byte| byte == 0 ? 256 : byte }
440
+ end
441
+ alias_method :parse_size_for_cur, :parse_size_for_ico
442
+
443
+ def parse_size_for_gif
444
+ @stream.read(11)[6..10].unpack('SS')
445
+ end
446
+
447
+ def parse_size_for_png
448
+ @stream.read(25)[16..24].unpack('NN')
449
+ end
450
+
451
+ def parse_size_for_jpeg
452
+ loop do
453
+ @state = case @state
454
+ when nil
455
+ @stream.read(2)
456
+ :started
457
+ when :started
458
+ @stream.read_byte == 0xFF ? :sof : :started
459
+ when :sof
460
+ case @stream.read_byte
461
+ when 0xe1 # APP1
462
+ skip_chars = @stream.read_int - 2
463
+ data = @stream.read(skip_chars)
464
+ io = StringIO.new(data)
465
+ if io.read(4) == "Exif"
466
+ io.read(2)
467
+ @exif = Exif.new(IOStream.new(io)) rescue nil
468
+ end
469
+ :started
470
+ when 0xe0..0xef
471
+ :skipframe
472
+ when 0xC0..0xC3, 0xC5..0xC7, 0xC9..0xCB, 0xCD..0xCF
473
+ :readsize
474
+ when 0xFF
475
+ :sof
476
+ else
477
+ :skipframe
478
+ end
479
+ when :skipframe
480
+ skip_chars = @stream.read_int - 2
481
+ @stream.read(skip_chars)
482
+ :started
483
+ when :readsize
484
+ _s = @stream.read(3)
485
+ height = @stream.read_int
486
+ width = @stream.read_int
487
+ width, height = height, width if @exif && @exif.rotated?
488
+ return [width, height]
489
+ end
490
+ end
491
+ end
492
+
493
+ def parse_size_for_bmp
494
+ d = @stream.read(32)[14..28]
495
+ header = d.unpack("C")[0]
496
+
497
+ result = if header == 40
498
+ d[4..-1].unpack('l<l<')
499
+ else
500
+ d[4..8].unpack('SS')
501
+ end
502
+
503
+ # ImageHeight is expressed in pixels. The absolute value is necessary because ImageHeight can be negative
504
+ [result.first, result.last.abs]
505
+ end
506
+
507
+ def parse_size_for_webp
508
+ vp8 = @stream.read(16)[12..15]
509
+ _len = @stream.read(4).unpack("V")
510
+ case vp8
511
+ when "VP8 "
512
+ parse_size_vp8
513
+ when "VP8L"
514
+ parse_size_vp8l
515
+ when "VP8X"
516
+ parse_size_vp8x
517
+ else
518
+ nil
519
+ end
520
+ end
521
+
522
+ def parse_size_vp8
523
+ w, h = @stream.read(10).unpack("@6vv")
524
+ [w & 0x3fff, h & 0x3fff]
525
+ end
526
+
527
+ def parse_size_vp8l
528
+ @stream.read(1) # 0x2f
529
+ b1, b2, b3, b4 = @stream.read(4).bytes.to_a
530
+ [1 + (((b2 & 0x3f) << 8) | b1), 1 + (((b4 & 0xF) << 10) | (b3 << 2) | ((b2 & 0xC0) >> 6))]
531
+ end
532
+
533
+ def parse_size_vp8x
534
+ flags = @stream.read(4).unpack("C")[0]
535
+ b1, b2, b3, b4, b5, b6 = @stream.read(6).unpack("CCCCCC")
536
+ width, height = 1 + b1 + (b2 << 8) + (b3 << 16), 1 + b4 + (b5 << 8) + (b6 << 16)
537
+
538
+ if flags & 8 > 0 # exif
539
+ # parse exif for orientation
540
+ # TODO: find or create test images for this
541
+ end
542
+
543
+ return [width, height]
544
+ end
545
+
546
+ class Exif # :nodoc:
547
+ attr_reader :width, :height
548
+ def initialize(stream)
549
+ @stream = stream
550
+ parse_exif
551
+ end
552
+
553
+ def rotated?
554
+ @orientation && @orientation >= 5
555
+ end
556
+
557
+ private
558
+
559
+ def get_exif_byte_order
560
+ byte_order = @stream.read(2)
561
+ case byte_order
562
+ when 'II'
563
+ @short, @long = 'v', 'V'
564
+ when 'MM'
565
+ @short, @long = 'n', 'N'
566
+ else
567
+ raise CannotParseImage
568
+ end
569
+ end
570
+
571
+ def parse_exif_ifd
572
+ tag_count = @stream.read(2).unpack(@short)[0]
573
+ tag_count.downto(1) do
574
+ type = @stream.read(2).unpack(@short)[0]
575
+ @stream.read(6)
576
+ data = @stream.read(2).unpack(@short)[0]
577
+ case type
578
+ when 0x0100 # image width
579
+ @width = data
580
+ when 0x0101 # image height
581
+ @height = data
582
+ when 0x0112 # orientation
583
+ @orientation = data
584
+ end
585
+ if @width && @height && @orientation
586
+ return # no need to parse more
587
+ end
588
+ @stream.read(2)
589
+ end
590
+ end
591
+
592
+ def parse_exif
593
+ @start_byte = @stream.pos
594
+
595
+ get_exif_byte_order
596
+
597
+ @stream.read(2) # 42
598
+
599
+ offset = @stream.read(4).unpack(@long)[0]
600
+ @stream.read(offset - 8)
601
+
602
+ parse_exif_ifd
603
+ end
604
+
605
+ end
606
+
607
+ def parse_size_for_tiff
608
+ exif = Exif.new(@stream)
609
+ if exif.rotated?
610
+ [exif.height, exif.width]
611
+ else
612
+ [exif.width, exif.height]
613
+ end
614
+ end
615
+
616
+ def parse_size_for_psd
617
+ @stream.read(26).unpack("x14NN").reverse
618
+ end
619
+ end