discourse_fastimage 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.textile +155 -0
  4. data/lib/fastimage.rb +739 -0
  5. metadata +119 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 560480951f53e6ba181d39677045efc544493d0d
4
+ data.tar.gz: 9643cf0faf379cfc49b068f0cb46a29f027eb7a9
5
+ SHA512:
6
+ metadata.gz: 29ec6c64ddb6f59d22c7a22ed5b0777095b5338cc9938fc53a58f2b6e66db92680e04f59a6b737a46133935285a1495d6fa42c484a22742e361f90c57e97592f
7
+ data.tar.gz: d64ce2e11be02ff4c4849ecf30ba0ee7d1d9c0c293e19351f3600669f945bff5fb8479e05901aa8e16aadd1614d7903e4be692e2d73994fddacf9f80125c7fc3
data/MIT-LICENSE ADDED
@@ -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.
data/README.textile ADDED
@@ -0,0 +1,155 @@
1
+ !https://img.shields.io/gem/dt/fastimage.svg!:https://rubygems.org/gems/fastimage
2
+ !https://travis-ci.org/sdsykes/fastimage.png?branch=master!:https://travis-ci.org/sdsykes/fastimage
3
+
4
+ h1. FastImage
5
+
6
+ h4. FastImage finds the size or type of an image given its uri by fetching as little as needed
7
+
8
+ h2. The problem
9
+
10
+ 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.
11
+
12
+ But the image is not locally stored - it's on another asset server, or in the cloud - at Amazon S3 for example.
13
+
14
+ 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.
15
+
16
+ FastImage does this minimal fetch for image types GIF, JPEG, PNG, TIFF, BMP, ICO, CUR, PSD, SVG 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).
17
+
18
+ You only need supply the uri, and FastImage will do the rest.
19
+
20
+ h2. Features
21
+
22
+ Fastimage can also read local (and other) files - anything that is not parseable as a URI will be
23
+ interpreted as a filename, and FastImage will attempt to open it with File#open.
24
+
25
+ FastImage will also automatically read from any object that responds to :read - for
26
+ instance an IO object if that is passed instead of a URI.
27
+
28
+ FastImage will follow up to 4 HTTP redirects to get the image.
29
+
30
+ 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.
31
+
32
+ You can add a timeout to the request which will limit the request time by passing :timeout => number_of_seconds.
33
+
34
+ FastImage normally replies will nil if it encounters an error, but you can pass :raise_on_failure => true to get an exception.
35
+
36
+ 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.
37
+
38
+ 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'}.
39
+
40
+ FastImage can give you information about the parsed display orientation of an image with Exif data (jpeg or tiff).
41
+
42
+ h2. Security
43
+
44
+ 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.
45
+
46
+ h2. Examples
47
+
48
+ <pre lang="ruby"><code>
49
+ require 'fastimage'
50
+
51
+ FastImage.size("http://stephensykes.com/images/ss.com_x.gif")
52
+ => [266, 56] # width, height
53
+ FastImage.type("http://stephensykes.com/images/pngimage")
54
+ => :png
55
+ FastImage.type("/some/local/file.gif")
56
+ => :gif
57
+ FastImage.size("http://upload.wikimedia.org/wikipedia/commons/b/b4/Mardin_1350660_1350692_33_images.jpg", :raise_on_failure=>true, :timeout=>0.1)
58
+ => FastImage::ImageFetchFailure: FastImage::ImageFetchFailure
59
+ FastImage.size("http://upload.wikimedia.org/wikipedia/commons/b/b4/Mardin_1350660_1350692_33_images.jpg", :raise_on_failure=>true, :timeout=>2.0)
60
+ => [9545, 6623]
61
+ FastImage.new("http://stephensykes.com/images/pngimage").content_length
62
+ => 432
63
+ FastImage.size("http://stephensykes.com/images/ss.com_x.gif", :http_header => {'User-Agent' => 'Fake Browser'})
64
+ => [266, 56]
65
+ FastImage.new("http://stephensykes.com/images/ExifOrientation3.jpg").orientation
66
+ => 3
67
+ </code></pre>
68
+
69
+ h2. Installation
70
+
71
+ h4. Required Ruby version
72
+
73
+ FastImage version 2.0.0 and above work with Ruby 1.9.2 and above.
74
+
75
+ FastImage version 1.9.0 was the last version that supported Ruby 1.8.7.
76
+
77
+ h4. Gem
78
+
79
+ bc. gem install fastimage
80
+
81
+ h4. Rails
82
+
83
+ Add fastimage to your Gemfile, and bundle.
84
+
85
+ bc. gem 'fastimage'
86
+
87
+ Then you're off - just use @FastImage.size()@ and @FastImage.type()@ in your code as in the examples.
88
+
89
+ h2. Documentation
90
+
91
+ "http://sdsykes.github.io/fastimage/rdoc/FastImage.html":http://sdsykes.github.io/fastimage/rdoc/FastImage.html
92
+
93
+ h2. Benchmark
94
+
95
+ It's way faster than conventional methods (for example the image_size gem) for most types of file when fetching over the wire.
96
+
97
+ <pre lang="ruby"><code>
98
+ irb> uri = "http://upload.wikimedia.org/wikipedia/commons/b/b4/Mardin_1350660_1350692_33_images.jpg"
99
+ irb> puts Benchmark.measure {open(uri, 'rb') {|fh| p ImageSize.new(fh).size}}
100
+ [9545, 6623]
101
+ 0.680000 0.250000 0.930000 ( 7.571887)
102
+
103
+ irb> puts Benchmark.measure {p FastImage.size(uri)}
104
+ [9545, 6623]
105
+ 0.010000 0.000000 0.010000 ( 0.090640)
106
+ </code></pre>
107
+
108
+ 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.
109
+
110
+ You'll see similar excellent results for the other file types, except for TIFF. Unfortunately TIFFs tend to have their
111
+ metadata towards the end of the file, so it makes little difference to do a minimal fetch. The result shown below is
112
+ mostly dependent on the exact internet conditions during the test, and little to do with the library used.
113
+
114
+ <pre lang="ruby"><code>
115
+ irb> uri = "http://upload.wikimedia.org/wikipedia/commons/1/11/Shinbutsureijoushuincho.tiff"
116
+ irb> puts Benchmark.measure {open(uri, 'rb') {|fh| p ImageSize.new(fh).size}}
117
+ [1120, 1559]
118
+ 1.080000 0.370000 1.450000 ( 13.766962)
119
+
120
+ irb> puts Benchmark.measure {p FastImage.size(uri)}
121
+ [1120, 1559]
122
+ 3.490000 3.810000 7.300000 ( 11.754315)
123
+ </code></pre>
124
+
125
+ h2. Tests
126
+
127
+ You'll need to @gem install fakeweb@ and possibly also @gem install test-unit@ to be able to run the tests.
128
+
129
+ bc.. $ ruby test.rb
130
+ Run options:
131
+
132
+ # Running tests:
133
+
134
+ Finished tests in 1.033640s, 23.2189 tests/s, 82.2337 assertions/s.
135
+ 24 tests, 85 assertions, 0 failures, 0 errors, 0 skips
136
+
137
+ h2. References
138
+
139
+ * "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/
140
+ * "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/
141
+ * "imagesize gem":https://rubygems.org/gems/imagesize
142
+ * "EXIF Reader":https://github.com/remvee/exifr
143
+
144
+ h2. FastImage in other languages
145
+
146
+ * "Python by bmuller":https://github.com/bmuller/fastimage
147
+ * "Swift by kaishin":https://github.com/kaishin/ImageScout
148
+ * "Go by rubenfonseca":https://github.com/rubenfonseca/fastimage
149
+ * "PHP by tommoor":https://github.com/tommoor/fastimage
150
+ * "Node.js by ShogunPanda":https://github.com/ShogunPanda/fastimage
151
+ * "Objective C by kylehickinson":https://github.com/kylehickinson/FastImage
152
+
153
+ h2. Licence
154
+
155
+ MIT, see file "MIT-LICENSE":MIT-LICENSE
data/lib/fastimage.rb ADDED
@@ -0,0 +1,739 @@
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, SVG 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 reads 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
+ # FastImage also provides a reader for the content length header provided in HTTP.
24
+ # This may be useful to assess the file size of an image, but do not rely on it exclusively -
25
+ # it will not be present in chunked responses for instance.
26
+ #
27
+ # FastImage accepts additional HTTP headers. This can be used to set a user agent
28
+ # or referrer which some servers require. Pass an :http_header argument to specify headers,
29
+ # e.g., :http_header => {'User-Agent' => 'Fake Browser'}.
30
+ #
31
+ # FastImage can give you information about the parsed display orientation of an image with Exif
32
+ # data (jpeg or tiff).
33
+ #
34
+ # === Examples
35
+ # require 'fastimage'
36
+ #
37
+ # FastImage.size("http://stephensykes.com/images/ss.com_x.gif")
38
+ # => [266, 56]
39
+ # FastImage.type("http://stephensykes.com/images/pngimage")
40
+ # => :png
41
+ # FastImage.type("/some/local/file.gif")
42
+ # => :gif
43
+ # File.open("/some/local/file.gif", "r") {|io| FastImage.type(io)}
44
+ # => :gif
45
+ # FastImage.new("http://stephensykes.com/images/pngimage").content_length
46
+ # => 432
47
+ # FastImage.new("http://stephensykes.com/images/ExifOrientation3.jpg").orientation
48
+ # => 3
49
+ #
50
+ # === References
51
+ # * http://www.anttikupila.com/flash/getting-jpg-dimensions-with-as3-without-loading-the-entire-file/
52
+ # * http://pennysmalls.wordpress.com/2008/08/19/find-jpeg-dimensions-fast-in-pure-ruby-no-ima/
53
+ # * https://rubygems.org/gems/imagesize
54
+ # * https://github.com/remvee/exifr
55
+ #
56
+
57
+ require 'net/https'
58
+ require 'open-uri'
59
+ require 'delegate'
60
+ require 'pathname'
61
+ require 'zlib'
62
+
63
+ class FastImage
64
+ attr_reader :size, :type, :content_length, :orientation
65
+
66
+ attr_reader :bytes_read
67
+
68
+ class FastImageException < StandardError # :nodoc:
69
+ end
70
+ class UnknownImageType < FastImageException # :nodoc:
71
+ end
72
+ class ImageFetchFailure < FastImageException # :nodoc:
73
+ end
74
+ class SizeNotFound < FastImageException # :nodoc:
75
+ end
76
+ class CannotParseImage < FastImageException # :nodoc:
77
+ end
78
+
79
+ DefaultTimeout = 2 unless const_defined?(:DefaultTimeout)
80
+
81
+ LocalFileChunkSize = 256 unless const_defined?(:LocalFileChunkSize)
82
+
83
+ # Parser object should respond to #parse
84
+ # and raise a URI::InvalidURIError if something goes wrong
85
+ def self.uri_parser=(parser)
86
+ @uri_parser = parser
87
+ end
88
+
89
+ # Helper that sets URI parsing to use the Addressable gem
90
+ def self.use_addressable_uri_parser
91
+ require 'addressable/uri'
92
+ self.uri_parser = Class.new do
93
+ def self.parse(location)
94
+ Addressable::URI.parse(location)
95
+ rescue Addressable::URI::InvalidURIError
96
+ raise URI::InvalidURIError
97
+ end
98
+ end
99
+ end
100
+
101
+ def self.parse_uri(location)
102
+ (@uri_parser || URI).parse(location)
103
+ end
104
+
105
+ # Returns an array containing the width and height of the image.
106
+ # It will return nil if the image could not be fetched, or if the image type was not recognised.
107
+ #
108
+ # By default there is a timeout of 2 seconds for opening and reading from a remote server.
109
+ # This can be changed by passing a :timeout => number_of_seconds in the options.
110
+ #
111
+ # If you wish FastImage to raise if it cannot size the image for any reason, then pass
112
+ # :raise_on_failure => true in the options.
113
+ #
114
+ # FastImage knows about GIF, JPEG, BMP, TIFF, ICO, CUR, PNG, PSD, SVG and WEBP files.
115
+ #
116
+ # === Example
117
+ #
118
+ # require 'fastimage'
119
+ #
120
+ # FastImage.size("http://stephensykes.com/images/ss.com_x.gif")
121
+ # => [266, 56]
122
+ # FastImage.size("http://stephensykes.com/images/pngimage")
123
+ # => [16, 16]
124
+ # FastImage.size("http://farm4.static.flickr.com/3023/3047236863_9dce98b836.jpg")
125
+ # => [500, 375]
126
+ # FastImage.size("http://www-ece.rice.edu/~wakin/images/lena512.bmp")
127
+ # => [512, 512]
128
+ # FastImage.size("test/fixtures/test.jpg")
129
+ # => [882, 470]
130
+ # FastImage.size("http://pennysmalls.com/does_not_exist")
131
+ # => nil
132
+ # FastImage.size("http://pennysmalls.com/does_not_exist", :raise_on_failure=>true)
133
+ # => raises FastImage::ImageFetchFailure
134
+ # FastImage.size("http://stephensykes.com/favicon.ico", :raise_on_failure=>true)
135
+ # => [16, 16]
136
+ # FastImage.size("http://stephensykes.com/images/squareBlue.icns", :raise_on_failure=>true)
137
+ # => raises FastImage::UnknownImageType
138
+ # FastImage.size("http://stephensykes.com/favicon.ico", :raise_on_failure=>true, :timeout=>0.01)
139
+ # => raises FastImage::ImageFetchFailure
140
+ # FastImage.size("http://stephensykes.com/images/faulty.jpg", :raise_on_failure=>true)
141
+ # => raises FastImage::SizeNotFound
142
+ #
143
+ # === Supported options
144
+ # [:timeout]
145
+ # Overrides the default timeout of 2 seconds. Applies both to reading from and opening the http connection.
146
+ # [:raise_on_failure]
147
+ # If set to true causes an exception to be raised if the image size cannot be found for any reason.
148
+ #
149
+ def self.size(uri, options={})
150
+ new(uri, options).size
151
+ end
152
+
153
+ # Returns an symbol indicating the image type fetched from a uri.
154
+ # It will return nil if the image could not be fetched, or if the image type was not recognised.
155
+ #
156
+ # By default there is a timeout of 2 seconds for opening and reading from a remote server.
157
+ # This can be changed by passing a :timeout => number_of_seconds in the options.
158
+ #
159
+ # If you wish FastImage to raise if it cannot find the type of the image for any reason, then pass
160
+ # :raise_on_failure => true in the options.
161
+ #
162
+ # === Example
163
+ #
164
+ # require 'fastimage'
165
+ #
166
+ # FastImage.type("http://stephensykes.com/images/ss.com_x.gif")
167
+ # => :gif
168
+ # FastImage.type("http://stephensykes.com/images/pngimage")
169
+ # => :png
170
+ # FastImage.type("http://farm4.static.flickr.com/3023/3047236863_9dce98b836.jpg")
171
+ # => :jpeg
172
+ # FastImage.type("http://www-ece.rice.edu/~wakin/images/lena512.bmp")
173
+ # => :bmp
174
+ # FastImage.type("test/fixtures/test.jpg")
175
+ # => :jpeg
176
+ # FastImage.type("http://stephensykes.com/does_not_exist")
177
+ # => nil
178
+ # File.open("/some/local/file.gif", "r") {|io| FastImage.type(io)}
179
+ # => :gif
180
+ # FastImage.type("test/fixtures/test.tiff")
181
+ # => :tiff
182
+ # FastImage.type("test/fixtures/test.psd")
183
+ # => :psd
184
+ #
185
+ # === Supported options
186
+ # [:timeout]
187
+ # Overrides the default timeout of 2 seconds. Applies both to reading from and opening the http connection.
188
+ # [:raise_on_failure]
189
+ # If set to true causes an exception to be raised if the image type cannot be found for any reason.
190
+ #
191
+ def self.type(uri, options={})
192
+ new(uri, options.merge(:type_only=>true)).type
193
+ end
194
+
195
+ def initialize(uri, options={})
196
+ @uri = uri
197
+ @options = {
198
+ :type_only => false,
199
+ :timeout => DefaultTimeout,
200
+ :raise_on_failure => false,
201
+ :proxy => nil,
202
+ :http_header => {}
203
+ }.merge(options)
204
+
205
+ @property = @options[:type_only] ? :type : :size
206
+
207
+ if uri.respond_to?(:read)
208
+ fetch_using_read(uri)
209
+ else
210
+ begin
211
+ @parsed_uri = self.class.parse_uri(uri)
212
+ rescue URI::InvalidURIError
213
+ fetch_using_file_open
214
+ else
215
+ if @parsed_uri.scheme == "http" || @parsed_uri.scheme == "https"
216
+ fetch_using_http
217
+ else
218
+ fetch_using_file_open
219
+ end
220
+ end
221
+ end
222
+
223
+ raise SizeNotFound if @options[:raise_on_failure] && @property == :size && !@size
224
+
225
+ rescue Timeout::Error, SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ECONNRESET,
226
+ ImageFetchFailure, Net::HTTPBadResponse, EOFError, Errno::ENOENT, IOError
227
+ raise ImageFetchFailure if @options[:raise_on_failure]
228
+ rescue NoMethodError # 1.8.7p248 can raise this due to a net/http bug
229
+ raise ImageFetchFailure if @options[:raise_on_failure]
230
+ rescue UnknownImageType
231
+ raise UnknownImageType if @options[:raise_on_failure]
232
+ rescue CannotParseImage
233
+ if @options[:raise_on_failure]
234
+ if @property == :size
235
+ raise SizeNotFound
236
+ else
237
+ raise ImageFetchFailure
238
+ end
239
+ end
240
+
241
+ ensure
242
+ uri.rewind if uri.respond_to?(:rewind)
243
+ end
244
+
245
+ private
246
+
247
+ def fetch_using_http
248
+ @redirect_count = 0
249
+
250
+ fetch_using_http_from_parsed_uri
251
+ end
252
+
253
+ def fetch_using_http_from_parsed_uri
254
+ http_header = {'Accept-Encoding' => 'identity'}.merge(@options[:http_header])
255
+
256
+ setup_http
257
+ @http.request_get(@parsed_uri.request_uri, http_header) do |res|
258
+ if res.is_a?(Net::HTTPRedirection) && @redirect_count < 4
259
+ @redirect_count += 1
260
+ begin
261
+ newly_parsed_uri = self.class.parse_uri(res['Location'])
262
+ # The new location may be relative - check for that
263
+ if newly_parsed_uri.scheme != "http" && newly_parsed_uri.scheme != "https"
264
+ @parsed_uri.path = res['Location']
265
+ else
266
+ @parsed_uri = newly_parsed_uri
267
+ end
268
+ rescue URI::InvalidURIError
269
+ else
270
+ fetch_using_http_from_parsed_uri
271
+ break
272
+ end
273
+ end
274
+
275
+ raise ImageFetchFailure unless res.is_a?(Net::HTTPSuccess)
276
+
277
+ @content_length = res.content_length
278
+
279
+ read_fiber = Fiber.new do
280
+ res.read_body do |str|
281
+ Fiber.yield str
282
+ end
283
+ end
284
+
285
+ case res['content-encoding']
286
+ when 'deflate', 'gzip', 'x-gzip'
287
+ begin
288
+ gzip = Zlib::GzipReader.new(FiberStream.new(read_fiber))
289
+ rescue FiberError, Zlib::GzipFile::Error
290
+ raise CannotParseImage
291
+ end
292
+
293
+ read_fiber = Fiber.new do
294
+ while data = gzip.readline
295
+ Fiber.yield data
296
+ end
297
+ end
298
+ end
299
+
300
+ parse_packets FiberStream.new(read_fiber)
301
+
302
+ break # needed to actively quit out of the fetch
303
+ end
304
+ end
305
+
306
+ def proxy_uri
307
+ begin
308
+ if @options[:proxy]
309
+ proxy = self.class.parse_uri(@options[:proxy])
310
+ else
311
+ proxy = ENV['http_proxy'] && ENV['http_proxy'] != "" ? self.class.parse_uri(ENV['http_proxy']) : nil
312
+ end
313
+ rescue URI::InvalidURIError
314
+ proxy = nil
315
+ end
316
+ proxy
317
+ end
318
+
319
+ def setup_http
320
+ proxy = proxy_uri
321
+ use_ssl = (@parsed_uri.scheme == "https")
322
+ port = @parsed_uri.port || (use_ssl ? 443 : 80)
323
+
324
+ if proxy
325
+ @http = Net::HTTP::Proxy(proxy.host, proxy.port).new(@parsed_uri.host, port)
326
+ else
327
+ @http = Net::HTTP.new(@parsed_uri.host, port)
328
+ end
329
+
330
+ @http.use_ssl = use_ssl
331
+ @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
332
+ @http.open_timeout = @options[:timeout]
333
+ @http.read_timeout = @options[:timeout]
334
+ end
335
+
336
+ def fetch_using_read(readable)
337
+ # Pathnames respond to read, but always return the first
338
+ # chunk of the file unlike an IO (even though the
339
+ # docuementation for it refers to IO). Need to supply
340
+ # an offset in this case.
341
+ if readable.is_a?(Pathname)
342
+ read_fiber = Fiber.new do
343
+ offset = 0
344
+ while str = readable.read(LocalFileChunkSize, offset)
345
+ Fiber.yield str
346
+ offset += LocalFileChunkSize
347
+ end
348
+ end
349
+ else
350
+ read_fiber = Fiber.new do
351
+ while str = readable.read(LocalFileChunkSize)
352
+ Fiber.yield str
353
+ end
354
+ end
355
+ end
356
+
357
+ parse_packets FiberStream.new(read_fiber)
358
+ end
359
+
360
+ def fetch_using_file_open
361
+ File.open(@uri) do |s|
362
+ fetch_using_read(s)
363
+ end
364
+ end
365
+
366
+ def parse_packets(stream)
367
+ @stream = stream
368
+
369
+ begin
370
+ result = send("parse_#{@property}")
371
+ if result
372
+ # extract exif orientation if it was found
373
+ if @property == :size && result.size == 3
374
+ @orientation = result.pop
375
+ else
376
+ @orientation = 1
377
+ end
378
+
379
+ instance_variable_set("@#{@property}", result)
380
+ else
381
+ raise CannotParseImage
382
+ end
383
+ rescue FiberError
384
+ raise CannotParseImage
385
+ end
386
+ end
387
+
388
+ def parse_size
389
+ @type = parse_type unless @type
390
+ send("parse_size_for_#{@type}")
391
+ end
392
+
393
+ module StreamUtil # :nodoc:
394
+ def read_byte
395
+ read(1)[0].ord
396
+ end
397
+
398
+ def read_int
399
+ read(2).unpack('n')[0]
400
+ end
401
+
402
+ def read_string_int
403
+ value = []
404
+ while read(1) =~ /(\d)/
405
+ value << $1
406
+ end
407
+ value.join.to_i
408
+ end
409
+ end
410
+
411
+ class FiberStream # :nodoc:
412
+ include StreamUtil
413
+ attr_reader :pos
414
+
415
+ def initialize(read_fiber)
416
+ @read_fiber = read_fiber
417
+ @pos = 0
418
+ @strpos = 0
419
+ @str = ''
420
+ end
421
+
422
+ def peek(n)
423
+ while @strpos + n - 1 >= @str.size
424
+ unused_str = @str[@strpos..-1]
425
+ new_string = @read_fiber.resume
426
+ raise CannotParseImage if !new_string
427
+
428
+ # we are dealing with bytes here, so force the encoding
429
+ new_string.force_encoding("ASCII-8BIT") if String.method_defined? :force_encoding
430
+
431
+ @str = unused_str + new_string
432
+ @strpos = 0
433
+ end
434
+
435
+ @str[@strpos..(@strpos + n - 1)]
436
+ end
437
+
438
+ def read(n)
439
+ result = peek(n)
440
+ @strpos += n
441
+ @pos += n
442
+ result
443
+ end
444
+ end
445
+
446
+ class IOStream < SimpleDelegator # :nodoc:
447
+ include StreamUtil
448
+ end
449
+
450
+ def parse_type
451
+ case @stream.peek(2)
452
+ when "BM"
453
+ :bmp
454
+ when "GI"
455
+ :gif
456
+ when 0xff.chr + 0xd8.chr
457
+ :jpeg
458
+ when 0x89.chr + "P"
459
+ :png
460
+ when "II", "MM"
461
+ :tiff
462
+ when '8B'
463
+ :psd
464
+ when "\0\0"
465
+ # ico has either a 1 (for ico format) or 2 (for cursor) at offset 3
466
+ case @stream.peek(3).bytes.to_a.last
467
+ when 1 then :ico
468
+ when 2 then :cur
469
+ end
470
+ when "RI"
471
+ if @stream.peek(12)[8..11] == "WEBP"
472
+ :webp
473
+ else
474
+ raise UnknownImageType
475
+ end
476
+ when "<s"
477
+ :svg
478
+ when "<?"
479
+ if @stream.peek(100).include?("<svg")
480
+ :svg
481
+ else
482
+ raise UnknownImageType
483
+ end
484
+ else
485
+ raise UnknownImageType
486
+ end
487
+ end
488
+
489
+ def parse_size_for_ico
490
+ icons = @stream.read(6)[4..5].unpack('v').first
491
+ sizes = icons.times.map { @stream.read(16).unpack('C2').map { |x| x == 0 ? 256 : x } }.sort_by { |w,h| w * h }
492
+ sizes.last
493
+ end
494
+ alias_method :parse_size_for_cur, :parse_size_for_ico
495
+
496
+ def parse_size_for_gif
497
+ @stream.read(11)[6..10].unpack('SS')
498
+ end
499
+
500
+ def parse_size_for_png
501
+ @stream.read(25)[16..24].unpack('NN')
502
+ end
503
+
504
+ def parse_size_for_jpeg
505
+ loop do
506
+ @state = case @state
507
+ when nil
508
+ @stream.read(2)
509
+ :started
510
+ when :started
511
+ @stream.read_byte == 0xFF ? :sof : :started
512
+ when :sof
513
+ case @stream.read_byte
514
+ when 0xe1 # APP1
515
+ skip_chars = @stream.read_int - 2
516
+ data = @stream.read(skip_chars)
517
+ io = StringIO.new(data)
518
+ if io.read(4) == "Exif"
519
+ io.read(2)
520
+ @exif = Exif.new(IOStream.new(io)) rescue nil
521
+ end
522
+ :started
523
+ when 0xe0..0xef
524
+ :skipframe
525
+ when 0xC0..0xC3, 0xC5..0xC7, 0xC9..0xCB, 0xCD..0xCF
526
+ :readsize
527
+ when 0xFF
528
+ :sof
529
+ else
530
+ :skipframe
531
+ end
532
+ when :skipframe
533
+ skip_chars = @stream.read_int - 2
534
+ @stream.read(skip_chars)
535
+ :started
536
+ when :readsize
537
+ _s = @stream.read(3)
538
+ height = @stream.read_int
539
+ width = @stream.read_int
540
+ width, height = height, width if @exif && @exif.rotated?
541
+ return [width, height, @exif ? @exif.orientation : 1]
542
+ end
543
+ end
544
+ end
545
+
546
+ def parse_size_for_bmp
547
+ d = @stream.read(32)[14..28]
548
+ header = d.unpack("C")[0]
549
+
550
+ result = if header == 40
551
+ d[4..-1].unpack('l<l<')
552
+ else
553
+ d[4..8].unpack('SS')
554
+ end
555
+
556
+ # ImageHeight is expressed in pixels. The absolute value is necessary because ImageHeight can be negative
557
+ [result.first, result.last.abs]
558
+ end
559
+
560
+ def parse_size_for_webp
561
+ vp8 = @stream.read(16)[12..15]
562
+ _len = @stream.read(4).unpack("V")
563
+ case vp8
564
+ when "VP8 "
565
+ parse_size_vp8
566
+ when "VP8L"
567
+ parse_size_vp8l
568
+ when "VP8X"
569
+ parse_size_vp8x
570
+ else
571
+ nil
572
+ end
573
+ end
574
+
575
+ def parse_size_vp8
576
+ w, h = @stream.read(10).unpack("@6vv")
577
+ [w & 0x3fff, h & 0x3fff]
578
+ end
579
+
580
+ def parse_size_vp8l
581
+ @stream.read(1) # 0x2f
582
+ b1, b2, b3, b4 = @stream.read(4).bytes.to_a
583
+ [1 + (((b2 & 0x3f) << 8) | b1), 1 + (((b4 & 0xF) << 10) | (b3 << 2) | ((b2 & 0xC0) >> 6))]
584
+ end
585
+
586
+ def parse_size_vp8x
587
+ flags = @stream.read(4).unpack("C")[0]
588
+ b1, b2, b3, b4, b5, b6 = @stream.read(6).unpack("CCCCCC")
589
+ width, height = 1 + b1 + (b2 << 8) + (b3 << 16), 1 + b4 + (b5 << 8) + (b6 << 16)
590
+
591
+ if flags & 8 > 0 # exif
592
+ # parse exif for orientation
593
+ # TODO: find or create test images for this
594
+ end
595
+
596
+ return [width, height]
597
+ end
598
+
599
+ class Exif # :nodoc:
600
+ attr_reader :width, :height, :orientation
601
+
602
+ def initialize(stream)
603
+ @stream = stream
604
+ parse_exif
605
+ end
606
+
607
+ def rotated?
608
+ @orientation >= 5
609
+ end
610
+
611
+ private
612
+
613
+ def get_exif_byte_order
614
+ byte_order = @stream.read(2)
615
+ case byte_order
616
+ when 'II'
617
+ @short, @long = 'v', 'V'
618
+ when 'MM'
619
+ @short, @long = 'n', 'N'
620
+ else
621
+ raise CannotParseImage
622
+ end
623
+ end
624
+
625
+ def parse_exif_ifd
626
+ tag_count = @stream.read(2).unpack(@short)[0]
627
+ tag_count.downto(1) do
628
+ type = @stream.read(2).unpack(@short)[0]
629
+ @stream.read(6)
630
+ data = @stream.read(2).unpack(@short)[0]
631
+ case type
632
+ when 0x0100 # image width
633
+ @width = data
634
+ when 0x0101 # image height
635
+ @height = data
636
+ when 0x0112 # orientation
637
+ @orientation = data
638
+ end
639
+ if @width && @height && @orientation
640
+ return # no need to parse more
641
+ end
642
+ @stream.read(2)
643
+ end
644
+ end
645
+
646
+ def parse_exif
647
+ @start_byte = @stream.pos
648
+
649
+ get_exif_byte_order
650
+
651
+ @stream.read(2) # 42
652
+
653
+ offset = @stream.read(4).unpack(@long)[0]
654
+ @stream.read(offset - 8)
655
+
656
+ parse_exif_ifd
657
+
658
+ @orientation ||= 1
659
+ end
660
+
661
+ end
662
+
663
+ def parse_size_for_tiff
664
+ exif = Exif.new(@stream)
665
+ if exif.rotated?
666
+ [exif.height, exif.width, exif.orientation]
667
+ else
668
+ [exif.width, exif.height, exif.orientation]
669
+ end
670
+ end
671
+
672
+ def parse_size_for_psd
673
+ @stream.read(26).unpack("x14NN").reverse
674
+ end
675
+
676
+ class Svg # :nodoc:
677
+ def initialize(stream)
678
+ @stream = stream
679
+ parse_svg
680
+ end
681
+
682
+ def width_and_height
683
+ if @width && @height
684
+ [@width, @height]
685
+ elsif @width && @ratio
686
+ [@width, @width / @ratio]
687
+ elsif @height && @ratio
688
+ [@height * @ratio, @height]
689
+ end
690
+ end
691
+
692
+ private
693
+
694
+ def parse_svg
695
+ attr_name = []
696
+ state = nil
697
+
698
+ while (char = @stream.read(1)) && state != :stop do
699
+ case char
700
+ when "="
701
+ if attr_name.join =~ /width/i
702
+ @stream.read(1)
703
+ @width = @stream.read_string_int
704
+ return if @height
705
+ elsif attr_name.join =~ /height/i
706
+ @stream.read(1)
707
+ @height = @stream.read_string_int
708
+ return if @width
709
+ elsif attr_name.join =~ /viewbox/i
710
+ values = attr_value.split(/\s/)
711
+ @ratio = values[2].to_f / values[3].to_f
712
+ end
713
+ when /\w/
714
+ attr_name << char
715
+ when ">"
716
+ state = :stop if state == :started
717
+ else
718
+ state = :started if attr_name.join == "svg"
719
+ attr_name.clear
720
+ end
721
+ end
722
+ end
723
+
724
+ def attr_value
725
+ @stream.read(1)
726
+
727
+ value = []
728
+ while @stream.read(1) =~ /([^"])/
729
+ value << $1
730
+ end
731
+ value.join
732
+ end
733
+ end
734
+
735
+ def parse_size_for_svg
736
+ svg = Svg.new(@stream)
737
+ svg.width_and_height
738
+ end
739
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: discourse_fastimage
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Stephen Sykes
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-05-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: addressable
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: fakeweb
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.5'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rdoc
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: test-unit
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: FastImage finds the size or type of an image given its uri by fetching
84
+ as little as needed.
85
+ email: sdsykes@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files:
89
+ - README.textile
90
+ files:
91
+ - MIT-LICENSE
92
+ - README.textile
93
+ - lib/fastimage.rb
94
+ homepage: http://github.com/discourse/fastimage
95
+ licenses:
96
+ - MIT
97
+ metadata: {}
98
+ post_install_message:
99
+ rdoc_options:
100
+ - "--charset=UTF-8"
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: 1.9.2
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubyforge_project:
115
+ rubygems_version: 2.5.1
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: FastImage - Image info fast
119
+ test_files: []