fastimage 2.3.1 → 2.4.0

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
2
  SHA256:
3
- metadata.gz: e4eedb99b6314f24fb47381046bf63595b90b8eb8045a6b9b238a92acebc00ba
4
- data.tar.gz: 10db6080223fb596a5fcfc904563a5f25b8209af1c4166260831e7aef9d7613e
3
+ metadata.gz: f76a7f8a37bf8a8e98ea4d1e740a0df0d1248005bb143b2a0ffb5f6a27ad5ae5
4
+ data.tar.gz: 9b12393ce757e0628f298cb2919475ceeacd660dbf41f55fb98c8d0d3871e1d9
5
5
  SHA512:
6
- metadata.gz: eae9ad37f27b853c2927183802b015ca6a066e413a112d80bb119603944d07d9fba5aac36712bc12c12acb327e3031a6b8c6875120e6f8de501e0526d4e2be14
7
- data.tar.gz: 2b8e2add3af30fe7e856b84e18ba6f9425fb939e5e789f1601fb360abb91ee5197b15adaa6cff79a2bdb89ea7a9d95306ebc0513c4882cd0a30b7ab3261173d2
6
+ metadata.gz: 2bc5b3c37baafd7c2bf9bdddddc5464f2a1963c65e47a263b73e00251e3d763391e65afddc5fadf5fc87a76f6751e3c8b013c6578b32ff65a62cf885a4b2dafa
7
+ data.tar.gz: b1c2973319aa246147e80ad42776104e3317ca2f5e5ce154c512ad4157a9f1cc04d4ed1547031580f3252a14fd479c3a4cea671ff731b5739a5775565ca0af5d
data/README.md CHANGED
@@ -13,7 +13,7 @@ But the image is not locally stored - it's on another asset server, or in the cl
13
13
 
14
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
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).
16
+ FastImage does this minimal fetch for image types GIF, JPEG, PNG, TIFF, BMP, ICO, CUR, PSD, SVG, WEBP and JXL. 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
17
 
18
18
  You only need supply the uri, and FastImage will do the rest.
19
19
 
@@ -196,6 +196,10 @@ ruby test/test.rb
196
196
  - [Android by qstumn](https://github.com/qstumn/FastImageSize)
197
197
  - [Flutter by ky1vstar](https://github.com/ky1vstar/fastimage.dart)
198
198
 
199
+ ### Also of interest
200
+ - [C++ by xiaozhuai](https://github.com/xiaozhuai/imageinfo)
201
+ - [Rust by xiaozhuai](https://github.com/xiaozhuai/imageinfo-rs)
202
+
199
203
  ## Licence
200
204
 
201
205
  MIT, see file "MIT-LICENSE"
@@ -0,0 +1,471 @@
1
+ require_relative 'fastimage_parsing/image_base'
2
+ require_relative 'fastimage_parsing/stream_util'
3
+
4
+ require_relative 'fastimage_parsing/avif'
5
+ require_relative 'fastimage_parsing/bmp'
6
+ require_relative 'fastimage_parsing/exif'
7
+ require_relative 'fastimage_parsing/fiber_stream'
8
+ require_relative 'fastimage_parsing/gif'
9
+ require_relative 'fastimage_parsing/heic'
10
+ require_relative 'fastimage_parsing/ico'
11
+ require_relative 'fastimage_parsing/iso_bmff'
12
+ require_relative 'fastimage_parsing/jpeg'
13
+ require_relative 'fastimage_parsing/jxl'
14
+ require_relative 'fastimage_parsing/jxlc'
15
+ require_relative 'fastimage_parsing/png'
16
+ require_relative 'fastimage_parsing/psd'
17
+ require_relative 'fastimage_parsing/svg'
18
+ require_relative 'fastimage_parsing/tiff'
19
+ require_relative 'fastimage_parsing/type_parser'
20
+ require_relative 'fastimage_parsing/webp'
21
+
22
+ class FastImage
23
+ include FastImageParsing
24
+
25
+ attr_reader :bytes_read
26
+
27
+ class FastImageException < StandardError # :nodoc:
28
+ end
29
+ class UnknownImageType < FastImageException # :nodoc:
30
+ end
31
+ class ImageFetchFailure < FastImageException # :nodoc:
32
+ end
33
+ class SizeNotFound < FastImageException # :nodoc:
34
+ end
35
+ class CannotParseImage < FastImageException # :nodoc:
36
+ end
37
+ class BadImageURI < FastImageException # :nodoc:
38
+ end
39
+
40
+ DefaultTimeout = 2 unless const_defined?(:DefaultTimeout)
41
+
42
+ LocalFileChunkSize = 256 unless const_defined?(:LocalFileChunkSize)
43
+
44
+ private
45
+
46
+ Parsers = {
47
+ :bmp => Bmp,
48
+ :gif => Gif,
49
+ :jpeg => Jpeg,
50
+ :png => Png,
51
+ :tiff => Tiff,
52
+ :psd => Psd,
53
+ :heic => Heic,
54
+ :heif => Heic,
55
+ :webp => Webp,
56
+ :svg => Svg,
57
+ :ico => Ico,
58
+ :cur => Ico,
59
+ :jxl => Jxl,
60
+ :avif => Avif
61
+ }.freeze
62
+
63
+ public
64
+
65
+ SUPPORTED_IMAGE_TYPES = Parsers.keys.freeze
66
+
67
+ # Returns an array containing the width and height of the image.
68
+ # It will return nil if the image could not be fetched, or if the image type was not recognised.
69
+ #
70
+ # By default there is a timeout of 2 seconds for opening and reading from a remote server.
71
+ # This can be changed by passing a :timeout => number_of_seconds in the options.
72
+ #
73
+ # If you wish FastImage to raise if it cannot size the image for any reason, then pass
74
+ # :raise_on_failure => true in the options.
75
+ #
76
+ # FastImage knows about GIF, JPEG, BMP, TIFF, ICO, CUR, PNG, HEIC/HEIF, AVIF, PSD, SVG, WEBP and JXL files.
77
+ #
78
+ # === Example
79
+ #
80
+ # require 'fastimage'
81
+ #
82
+ # FastImage.size("https://switchstep.com/images/ios.gif")
83
+ # => [196, 283]
84
+ # FastImage.size("http://switchstep.com/images/ss_logo.png")
85
+ # => [300, 300]
86
+ # FastImage.size("https://upload.wikimedia.org/wikipedia/commons/0/09/Jpeg_thumb_artifacts_test.jpg")
87
+ # => [1280, 800]
88
+ # FastImage.size("https://eeweb.engineering.nyu.edu/~yao/EL5123/image/lena_gray.bmp")
89
+ # => [512, 512]
90
+ # FastImage.size("test/fixtures/test.jpg")
91
+ # => [882, 470]
92
+ # FastImage.size("http://switchstep.com/does_not_exist")
93
+ # => nil
94
+ # FastImage.size("http://switchstep.com/does_not_exist", :raise_on_failure=>true)
95
+ # => raises FastImage::ImageFetchFailure
96
+ # FastImage.size("http://switchstep.com/images/favicon.ico", :raise_on_failure=>true)
97
+ # => [16, 16]
98
+ # FastImage.size("http://switchstep.com/foo.ics", :raise_on_failure=>true)
99
+ # => raises FastImage::UnknownImageType
100
+ # FastImage.size("http://switchstep.com/images/favicon.ico", :raise_on_failure=>true, :timeout=>0.01)
101
+ # => raises FastImage::ImageFetchFailure
102
+ # FastImage.size("http://switchstep.com/images/faulty.jpg", :raise_on_failure=>true)
103
+ # => raises FastImage::SizeNotFound
104
+ #
105
+ # === Supported options
106
+ # [:timeout]
107
+ # Overrides the default timeout of 2 seconds. Applies both to reading from and opening the http connection.
108
+ # [:raise_on_failure]
109
+ # If set to true causes an exception to be raised if the image size cannot be found for any reason.
110
+ #
111
+ def self.size(uri, options={})
112
+ new(uri, options).size
113
+ end
114
+
115
+ # Returns an symbol indicating the image type fetched from a uri.
116
+ # It will return nil if the image could not be fetched, or if the image type was not recognised.
117
+ #
118
+ # By default there is a timeout of 2 seconds for opening and reading from a remote server.
119
+ # This can be changed by passing a :timeout => number_of_seconds in the options.
120
+ #
121
+ # If you wish FastImage to raise if it cannot find the type of the image for any reason, then pass
122
+ # :raise_on_failure => true in the options.
123
+ #
124
+ # === Example
125
+ #
126
+ # require 'fastimage'
127
+ #
128
+ # FastImage.type("https://switchstep.com/images/ios.gif")
129
+ # => :gif
130
+ # FastImage.type("http://switchstep.com/images/ss_logo.png")
131
+ # => :png
132
+ # FastImage.type("https://upload.wikimedia.org/wikipedia/commons/0/09/Jpeg_thumb_artifacts_test.jpg")
133
+ # => :jpeg
134
+ # FastImage.type("https://eeweb.engineering.nyu.edu/~yao/EL5123/image/lena_gray.bmp")
135
+ # => :bmp
136
+ # FastImage.type("test/fixtures/test.jpg")
137
+ # => :jpeg
138
+ # FastImage.type("http://switchstep.com/does_not_exist")
139
+ # => nil
140
+ # File.open("/some/local/file.gif", "r") {|io| FastImage.type(io)}
141
+ # => :gif
142
+ # FastImage.type("test/fixtures/test.tiff")
143
+ # => :tiff
144
+ # FastImage.type("test/fixtures/test.psd")
145
+ # => :psd
146
+ #
147
+ # === Supported options
148
+ # [:timeout]
149
+ # Overrides the default timeout of 2 seconds. Applies both to reading from and opening the http connection.
150
+ # [:raise_on_failure]
151
+ # If set to true causes an exception to be raised if the image type cannot be found for any reason.
152
+ #
153
+ def self.type(uri, options={})
154
+ new(uri, options).type
155
+ end
156
+
157
+ # Returns a boolean value indicating the image is animated.
158
+ # It will return nil if the image could not be fetched, or if the image type was not recognised.
159
+ #
160
+ # By default there is a timeout of 2 seconds for opening and reading from a remote server.
161
+ # This can be changed by passing a :timeout => number_of_seconds in the options.
162
+ #
163
+ # If you wish FastImage to raise if it cannot find the type of the image for any reason, then pass
164
+ # :raise_on_failure => true in the options.
165
+ #
166
+ # === Example
167
+ #
168
+ # require 'fastimage'
169
+ #
170
+ # FastImage.animated?("test/fixtures/test.gif")
171
+ # => false
172
+ # FastImage.animated?("test/fixtures/animated.gif")
173
+ # => true
174
+ #
175
+ # === Supported options
176
+ # [:timeout]
177
+ # Overrides the default timeout of 2 seconds. Applies both to reading from and opening the http connection.
178
+ # [:raise_on_failure]
179
+ # If set to true causes an exception to be raised if the image type cannot be found for any reason.
180
+ #
181
+ def self.animated?(uri, options={})
182
+ new(uri, options).animated
183
+ end
184
+
185
+ def initialize(uri, options={})
186
+ @uri = uri
187
+ @options = {
188
+ :timeout => DefaultTimeout,
189
+ :raise_on_failure => false,
190
+ :proxy => nil,
191
+ :http_header => {}
192
+ }.merge(options)
193
+ end
194
+
195
+ def type
196
+ @property = :type
197
+ fetch unless defined?(@type)
198
+ @type
199
+ end
200
+
201
+ def size
202
+ @property = :size
203
+ begin
204
+ fetch unless defined?(@size)
205
+ rescue CannotParseImage
206
+ end
207
+
208
+ raise SizeNotFound if @options[:raise_on_failure] && !@size
209
+
210
+ @size
211
+ end
212
+
213
+ def orientation
214
+ size unless defined?(@size)
215
+ @orientation ||= 1 if @size
216
+ end
217
+
218
+ def width
219
+ size && @size[0]
220
+ end
221
+
222
+ def height
223
+ size && @size[1]
224
+ end
225
+
226
+ def animated
227
+ @property = :animated
228
+ fetch unless defined?(@animated)
229
+ @animated
230
+ end
231
+
232
+ def content_length
233
+ @property = :content_length
234
+ fetch unless defined?(@content_length)
235
+ @content_length
236
+ end
237
+
238
+ # find an appropriate method to fetch the image according to the passed parameter
239
+ def fetch
240
+ raise BadImageURI if @uri.nil?
241
+
242
+ if @uri.respond_to?(:read)
243
+ fetch_using_read(@uri)
244
+ elsif @uri.start_with?('data:')
245
+ fetch_using_base64(@uri)
246
+ else
247
+ begin
248
+ @parsed_uri = URI.parse(@uri)
249
+ rescue URI::InvalidURIError
250
+ fetch_using_file_open
251
+ else
252
+ if @parsed_uri.scheme == "http" || @parsed_uri.scheme == "https"
253
+ fetch_using_http
254
+ else
255
+ fetch_using_file_open
256
+ end
257
+ end
258
+ end
259
+
260
+ raise SizeNotFound if @options[:raise_on_failure] && @property == :size && !@size
261
+
262
+ rescue Timeout::Error, SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ECONNRESET,
263
+ Errno::ENETUNREACH, ImageFetchFailure, Net::HTTPBadResponse, EOFError, Errno::ENOENT,
264
+ OpenSSL::SSL::SSLError
265
+ raise ImageFetchFailure if @options[:raise_on_failure]
266
+ rescue UnknownImageType, BadImageURI, CannotParseImage
267
+ raise if @options[:raise_on_failure]
268
+
269
+ ensure
270
+ @uri.rewind if @uri.respond_to?(:rewind)
271
+
272
+ end
273
+
274
+ private
275
+
276
+ def fetch_using_http
277
+ @redirect_count = 0
278
+
279
+ fetch_using_http_from_parsed_uri
280
+ end
281
+
282
+ # Some invalid locations need escaping
283
+ def escaped_location(location)
284
+ begin
285
+ URI(location)
286
+ rescue URI::InvalidURIError
287
+ ::URI::DEFAULT_PARSER.escape(location)
288
+ else
289
+ location
290
+ end
291
+ end
292
+
293
+ def fetch_using_http_from_parsed_uri
294
+ raise ImageFetchFailure unless @parsed_uri.is_a?(URI::HTTP)
295
+
296
+ http_header = {'Accept-Encoding' => 'identity'}.merge(@options[:http_header])
297
+
298
+ setup_http
299
+ @http.request_get(@parsed_uri.request_uri, http_header) do |res|
300
+ if res.is_a?(Net::HTTPRedirection) && @redirect_count < 4
301
+ @redirect_count += 1
302
+ begin
303
+ location = res['Location']
304
+ raise ImageFetchFailure if location.nil? || location.empty?
305
+
306
+ @parsed_uri = URI.join(@parsed_uri, escaped_location(location))
307
+ rescue URI::InvalidURIError
308
+ else
309
+ fetch_using_http_from_parsed_uri
310
+ break
311
+ end
312
+ end
313
+
314
+ raise ImageFetchFailure unless res.is_a?(Net::HTTPSuccess)
315
+
316
+ @content_length = res.content_length
317
+ break if @property == :content_length
318
+
319
+ read_fiber = Fiber.new do
320
+ res.read_body do |str|
321
+ Fiber.yield str
322
+ end
323
+ nil
324
+ end
325
+
326
+ case res['content-encoding']
327
+ when 'deflate', 'gzip', 'x-gzip'
328
+ begin
329
+ gzip = Zlib::GzipReader.new(FiberStream.new(read_fiber))
330
+ rescue FiberError, Zlib::GzipFile::Error
331
+ raise CannotParseImage
332
+ end
333
+
334
+ read_fiber = Fiber.new do
335
+ while data = gzip.readline
336
+ Fiber.yield data
337
+ end
338
+ nil
339
+ end
340
+ end
341
+
342
+ parse_packets FiberStream.new(read_fiber)
343
+
344
+ break # needed to actively quit out of the fetch
345
+ end
346
+ end
347
+
348
+ def protocol_relative_url?(url)
349
+ url.start_with?("//")
350
+ end
351
+
352
+ def proxy_uri
353
+ begin
354
+ if @options[:proxy]
355
+ proxy = URI.parse(@options[:proxy])
356
+ else
357
+ proxy = ENV['http_proxy'] && ENV['http_proxy'] != "" ? URI.parse(ENV['http_proxy']) : nil
358
+ end
359
+ rescue URI::InvalidURIError
360
+ proxy = nil
361
+ end
362
+ proxy
363
+ end
364
+
365
+ def setup_http
366
+ proxy = proxy_uri
367
+
368
+ if proxy
369
+ @http = Net::HTTP::Proxy(proxy.host, proxy.port, proxy.user, proxy.password).new(@parsed_uri.host, @parsed_uri.port)
370
+ else
371
+ @http = Net::HTTP.new(@parsed_uri.host, @parsed_uri.port)
372
+ end
373
+ @http.use_ssl = (@parsed_uri.scheme == "https")
374
+ @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
375
+ @http.open_timeout = @options[:timeout]
376
+ @http.read_timeout = @options[:timeout]
377
+ end
378
+
379
+ def fetch_using_read(readable)
380
+ return @content_length = readable.size if @property == :content_length && readable.respond_to?(:size)
381
+
382
+ readable.rewind if readable.respond_to?(:rewind)
383
+ # Pathnames respond to read, but always return the first
384
+ # chunk of the file unlike an IO (even though the
385
+ # docuementation for it refers to IO). Need to supply
386
+ # an offset in this case.
387
+ if readable.is_a?(Pathname)
388
+ read_fiber = Fiber.new do
389
+ offset = 0
390
+ while str = readable.read(LocalFileChunkSize, offset)
391
+ Fiber.yield str
392
+ offset += LocalFileChunkSize
393
+ end
394
+ nil
395
+ end
396
+ else
397
+ read_fiber = Fiber.new do
398
+ while str = readable.read(LocalFileChunkSize)
399
+ Fiber.yield str
400
+ end
401
+ nil
402
+ end
403
+ end
404
+
405
+ parse_packets FiberStream.new(read_fiber)
406
+ end
407
+
408
+ def fetch_using_file_open
409
+ return @content_length = File.size?(@uri) if @property == :content_length
410
+
411
+ File.open(@uri) do |s|
412
+ fetch_using_read(s)
413
+ end
414
+ end
415
+
416
+ def fetch_using_base64(uri)
417
+ decoded = begin
418
+ uri.split(',')[1].unpack("m").first
419
+ rescue
420
+ raise CannotParseImage
421
+ end
422
+
423
+ fetch_using_read StringIO.new(decoded)
424
+ end
425
+
426
+ def parse_packets(stream)
427
+ @stream = stream
428
+
429
+ begin
430
+ @type = TypeParser.new(@stream).type unless defined?(@type)
431
+
432
+ result = case @property
433
+ when :type
434
+ @type
435
+ when :size
436
+ parse_size
437
+ when :animated
438
+ parse_animated
439
+ end
440
+
441
+ if result != nil
442
+ # extract exif orientation if it was found
443
+ if @property == :size && result.size == 3
444
+ @orientation = result.pop
445
+ else
446
+ @orientation = 1
447
+ end
448
+
449
+ instance_variable_set("@#{@property}", result)
450
+ else
451
+ raise CannotParseImage
452
+ end
453
+ rescue FiberError
454
+ raise CannotParseImage
455
+ end
456
+ end
457
+
458
+ def parser_class
459
+ klass = Parsers[@type]
460
+ raise UnknownImageType unless klass
461
+ klass
462
+ end
463
+
464
+ def parse_size
465
+ parser_class.new(@stream).dimensions
466
+ end
467
+
468
+ def parse_animated
469
+ parser_class.new(@stream).animated?
470
+ end
471
+ end
@@ -0,0 +1,12 @@
1
+ module FastImageParsing
2
+ class Avif < ImageBase # :nodoc:
3
+ def dimensions
4
+ bmff = IsoBmff.new(@stream)
5
+ [bmff.width, bmff.height]
6
+ end
7
+
8
+ def animated?
9
+ @stream.peek(12)[4..-1] == "ftypavis"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ module FastImageParsing
2
+ class Bmp < ImageBase # :nodoc:
3
+ def dimensions
4
+ d = @stream.read(32)[14..28]
5
+ header = d.unpack("C")[0]
6
+
7
+ result = if header == 12
8
+ d[4..8].unpack('SS')
9
+ else
10
+ d[4..-1].unpack('l<l<')
11
+ end
12
+
13
+ # ImageHeight is expressed in pixels. The absolute value is necessary because ImageHeight can be negative
14
+ [result.first, result.last.abs]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,76 @@
1
+ module FastImageParsing
2
+ class Exif # :nodoc:
3
+ attr_reader :width, :height, :orientation
4
+
5
+ def initialize(stream)
6
+ @stream = stream
7
+ @width, @height, @orientation = nil
8
+ parse_exif
9
+ end
10
+
11
+ def rotated?
12
+ @orientation >= 5
13
+ end
14
+
15
+ private
16
+
17
+ def get_exif_byte_order
18
+ byte_order = @stream.read(2)
19
+ case byte_order
20
+ when 'II'
21
+ @short, @long = 'v', 'V'
22
+ when 'MM'
23
+ @short, @long = 'n', 'N'
24
+ else
25
+ raise FastImage::CannotParseImage
26
+ end
27
+ end
28
+
29
+ def parse_exif_ifd
30
+ tag_count = @stream.read(2).unpack(@short)[0]
31
+ tag_count.downto(1) do
32
+ type = @stream.read(2).unpack(@short)[0]
33
+ data_type = @stream.read(2).unpack(@short)[0]
34
+ @stream.read(4)
35
+
36
+ if data_type == 4
37
+ data = @stream.read(4).unpack(@long)[0]
38
+ else
39
+ data = @stream.read(2).unpack(@short)[0]
40
+ @stream.read(2)
41
+ end
42
+
43
+ case type
44
+ when 0x0100 # image width
45
+ @width = data
46
+ when 0x0101 # image height
47
+ @height = data
48
+ when 0x0112 # orientation
49
+ @orientation = data
50
+ end
51
+ if @width && @height && @orientation
52
+ return # no need to parse more
53
+ end
54
+ end
55
+ end
56
+
57
+ def parse_exif
58
+ @start_byte = @stream.pos
59
+
60
+ get_exif_byte_order
61
+
62
+ @stream.read(2) # 42
63
+
64
+ offset = @stream.read(4).unpack(@long)[0]
65
+ if @stream.respond_to?(:skip)
66
+ @stream.skip(offset - 8)
67
+ else
68
+ @stream.read(offset - 8)
69
+ end
70
+
71
+ parse_exif_ifd
72
+
73
+ @orientation ||= 1
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,58 @@
1
+ module FastImageParsing
2
+ class FiberStream # :nodoc:
3
+ include StreamUtil
4
+ attr_reader :pos
5
+
6
+ # read_fiber should return nil if it no longer has anything to return when resumed
7
+ # so the result of the whole Fiber block should be set to be nil in case yield is no
8
+ # longer called
9
+ def initialize(read_fiber)
10
+ @read_fiber = read_fiber
11
+ @pos = 0
12
+ @strpos = 0
13
+ @str = ''
14
+ end
15
+
16
+ # Peeking beyond the end of the input will raise
17
+ def peek(n)
18
+ while @strpos + n > @str.size
19
+ unused_str = @str[@strpos..-1]
20
+
21
+ new_string = @read_fiber.resume
22
+ raise FastImage::CannotParseImage if !new_string
23
+ # we are dealing with bytes here, so force the encoding
24
+ new_string.force_encoding("ASCII-8BIT") if new_string.respond_to? :force_encoding
25
+
26
+ @str = unused_str + new_string
27
+ @strpos = 0
28
+ end
29
+
30
+ @str[@strpos, n]
31
+ end
32
+
33
+ def read(n)
34
+ result = peek(n)
35
+ @strpos += n
36
+ @pos += n
37
+ result
38
+ end
39
+
40
+ def skip(n)
41
+ discarded = 0
42
+ fetched = @str[@strpos..-1].size
43
+ while n > fetched
44
+ discarded += @str[@strpos..-1].size
45
+ new_string = @read_fiber.resume
46
+ raise FastImage::CannotParseImage if !new_string
47
+
48
+ new_string.force_encoding("ASCII-8BIT") if new_string.respond_to? :force_encoding
49
+
50
+ fetched += new_string.size
51
+ @str = new_string
52
+ @strpos = 0
53
+ end
54
+ @strpos = @strpos + n - discarded
55
+ @pos += n
56
+ end
57
+ end
58
+ end