fastimage 2.1.7 → 2.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.textile +3 -2
  3. data/lib/fastimage.rb +297 -19
  4. metadata +8 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2aaa7bc9e7b596326f39fc933d836e8d94a0c19601670c0b55c839ba8d0e9ed5
4
- data.tar.gz: b807e4cb3ac6ac16b10c026d15ac6a6440a65a75036482518b1a337475bd723b
3
+ metadata.gz: 074e776db55bfd4766292bca262afc562d4d38d07d86f56f1299a666b4dc3e30
4
+ data.tar.gz: 7e079013cbd7802773119a5a6b7f25377e2878c1afbeb610c68d8a8a069ad0a1
5
5
  SHA512:
6
- metadata.gz: 8a87fd1e5eb5a209da0c2a2af1d7c0d6cead3f14af8cc591810e0e0d7b853cd519e236f9ec0ed70fdea0c39569631f5b41f88f5fea8942aa55fbe018390102ef
7
- data.tar.gz: 002d20c88557352e4b3ebb80975d2f26cffa221cc3c510a0865ffed55fe251a63f53dfeff53843f351574cff1aa9c9864d5df1a2e5fc9a4550b5e03fd798880a
6
+ metadata.gz: 4c82693c20723bc22b41e9839104097b0cb435c392e8ddd19edff93aeaa1802f3319c6ad49f1b82ba3926bd1bdcc5e04870d02401ff20e745e61d26a81473c6b
7
+ data.tar.gz: 230fbfd667aab5c59ce7bc53909454d2d9134fdb3c6705173d5df264a3c8a36df2d879c959df1ab4662d5e216a554ada7ba482ba3e0872b9c000d087c91e5b29
data/README.textile CHANGED
@@ -1,5 +1,5 @@
1
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
2
+ !https://travis-ci.org/sdsykes/fastimage.svg?branch=master!:https://travis-ci.org/sdsykes/fastimage
3
3
 
4
4
  h1. FastImage
5
5
 
@@ -132,7 +132,7 @@ h2. Tests
132
132
 
133
133
  You'll need to @gem install fakeweb@ and possibly also @gem install test-unit@ to be able to run the tests.
134
134
 
135
- bc.. $ ruby test.rb
135
+ bc.. $ ruby test/test.rb
136
136
  Run options:
137
137
 
138
138
  # Running tests:
@@ -156,6 +156,7 @@ h2. FastImage in other languages
156
156
  * "Node.js by ShogunPanda":https://github.com/ShogunPanda/fastimage
157
157
  * "Objective C by kylehickinson":https://github.com/kylehickinson/FastImage
158
158
  * "Android by qstumn":https://github.com/qstumn/FastImageSize
159
+ * "Flutter by ky1vstar":https://github.com/ky1vstar/fastimage.dart
159
160
 
160
161
  h2. Licence
161
162
 
data/lib/fastimage.rb CHANGED
@@ -70,7 +70,7 @@ if RUBY_VERSION < "2.2"
70
70
  end
71
71
 
72
72
  class FastImage
73
- attr_reader :size, :type, :content_length, :orientation
73
+ attr_reader :size, :type, :content_length, :orientation, :animated
74
74
 
75
75
  attr_reader :bytes_read
76
76
 
@@ -89,6 +89,8 @@ class FastImage
89
89
 
90
90
  LocalFileChunkSize = 256 unless const_defined?(:LocalFileChunkSize)
91
91
 
92
+ SUPPORTED_IMAGE_TYPES = [:bmp, :gif, :jpeg, :png, :tiff, :psd, :heic, :heif, :webp, :svg, :ico, :cur].freeze
93
+
92
94
  # Returns an array containing the width and height of the image.
93
95
  # It will return nil if the image could not be fetched, or if the image type was not recognised.
94
96
  #
@@ -179,6 +181,34 @@ class FastImage
179
181
  new(uri, options.merge(:type_only=>true)).type
180
182
  end
181
183
 
184
+ # Returns a boolean value indicating the image is animated.
185
+ # It will return nil if the image could not be fetched, or if the image type was not recognised.
186
+ #
187
+ # By default there is a timeout of 2 seconds for opening and reading from a remote server.
188
+ # This can be changed by passing a :timeout => number_of_seconds in the options.
189
+ #
190
+ # If you wish FastImage to raise if it cannot find the type of the image for any reason, then pass
191
+ # :raise_on_failure => true in the options.
192
+ #
193
+ # === Example
194
+ #
195
+ # require 'fastimage'
196
+ #
197
+ # FastImage.animated?("test/fixtures/test.gif")
198
+ # => false
199
+ # FastImage.animated?("test/fixtures/animated.gif")
200
+ # => true
201
+ #
202
+ # === Supported options
203
+ # [:timeout]
204
+ # Overrides the default timeout of 2 seconds. Applies both to reading from and opening the http connection.
205
+ # [:raise_on_failure]
206
+ # If set to true causes an exception to be raised if the image type cannot be found for any reason.
207
+ #
208
+ def self.animated?(uri, options={})
209
+ new(uri, options.merge(:animated_only=>true)).animated
210
+ end
211
+
182
212
  def initialize(uri, options={})
183
213
  @uri = uri
184
214
  @options = {
@@ -189,7 +219,13 @@ class FastImage
189
219
  :http_header => {}
190
220
  }.merge(options)
191
221
 
192
- @property = @options[:type_only] ? :type : :size
222
+ @property = if @options[:animated_only]
223
+ :animated
224
+ elsif @options[:type_only]
225
+ :type
226
+ else
227
+ :size
228
+ end
193
229
 
194
230
  @type, @state = nil
195
231
 
@@ -217,10 +253,8 @@ class FastImage
217
253
  Errno::ENETUNREACH, ImageFetchFailure, Net::HTTPBadResponse, EOFError, Errno::ENOENT,
218
254
  OpenSSL::SSL::SSLError
219
255
  raise ImageFetchFailure if @options[:raise_on_failure]
220
- rescue NoMethodError # 1.8.7p248 can raise this due to a net/http bug
221
- raise ImageFetchFailure if @options[:raise_on_failure]
222
256
  rescue UnknownImageType
223
- raise UnknownImageType if @options[:raise_on_failure]
257
+ raise if @options[:raise_on_failure]
224
258
  rescue CannotParseImage
225
259
  if @options[:raise_on_failure]
226
260
  if @property == :size
@@ -248,7 +282,7 @@ class FastImage
248
282
  begin
249
283
  URI(location)
250
284
  rescue URI::InvalidURIError
251
- URI.escape(location)
285
+ ::URI::DEFAULT_PARSER.escape(location)
252
286
  else
253
287
  location
254
288
  end
@@ -262,7 +296,10 @@ class FastImage
262
296
  if res.is_a?(Net::HTTPRedirection) && @redirect_count < 4
263
297
  @redirect_count += 1
264
298
  begin
265
- @parsed_uri = URI.join(@parsed_uri, escaped_location(res['Location']))
299
+ location = res['Location']
300
+ raise ImageFetchFailure if location.nil? || location.empty?
301
+
302
+ @parsed_uri = URI.join(@parsed_uri, escaped_location(location))
266
303
  rescue URI::InvalidURIError
267
304
  else
268
305
  fetch_using_http_from_parsed_uri
@@ -369,7 +406,7 @@ class FastImage
369
406
 
370
407
  begin
371
408
  result = send("parse_#{@property}")
372
- if result
409
+ if result != nil
373
410
  # extract exif orientation if it was found
374
411
  if @property == :size && result.size == 3
375
412
  @orientation = result.pop
@@ -391,9 +428,17 @@ class FastImage
391
428
  send("parse_size_for_#{@type}")
392
429
  end
393
430
 
431
+ def parse_animated
432
+ @type = parse_type unless @type
433
+ @type == :gif ? send("parse_animated_for_#{@type}") : nil
434
+ end
435
+
394
436
  def fetch_using_base64(uri)
395
- data = uri.split(',')[1]
396
- decoded = Base64.decode64(data)
437
+ decoded = begin
438
+ Base64.decode64(uri.split(',')[1])
439
+ rescue
440
+ raise CannotParseImage
441
+ end
397
442
  @content_length = decoded.size
398
443
  fetch_using_read StringIO.new(decoded)
399
444
  end
@@ -429,19 +474,20 @@ class FastImage
429
474
 
430
475
  # Peeking beyond the end of the input will raise
431
476
  def peek(n)
432
- while @strpos + n - 1 >= @str.size
477
+ while @strpos + n > @str.size
433
478
  unused_str = @str[@strpos..-1]
479
+
434
480
  new_string = @read_fiber.resume
481
+ new_string = @read_fiber.resume if new_string.is_a? Net::ReadAdapter
435
482
  raise CannotParseImage if !new_string
436
-
437
483
  # we are dealing with bytes here, so force the encoding
438
- new_string.force_encoding("ASCII-8BIT") if String.method_defined? :force_encoding
484
+ new_string.force_encoding("ASCII-8BIT") if new_string.respond_to? :force_encoding
439
485
 
440
486
  @str = unused_str + new_string
441
487
  @strpos = 0
442
488
  end
443
489
 
444
- @str[@strpos..(@strpos + n - 1)]
490
+ @str[@strpos, n]
445
491
  end
446
492
 
447
493
  def read(n)
@@ -459,7 +505,7 @@ class FastImage
459
505
  new_string = @read_fiber.resume
460
506
  raise CannotParseImage if !new_string
461
507
 
462
- new_string.force_encoding("ASCII-8BIT") if String.method_defined? :force_encoding
508
+ new_string.force_encoding("ASCII-8BIT") if new_string.respond_to? :force_encoding
463
509
 
464
510
  fetched += new_string.size
465
511
  @str = new_string
@@ -494,16 +540,29 @@ class FastImage
494
540
  when '8B'
495
541
  :psd
496
542
  when "\0\0"
497
- # ico has either a 1 (for ico format) or 2 (for cursor) at offset 3
498
543
  case @stream.peek(3).bytes.to_a.last
544
+ when 0
545
+ # http://www.ftyps.com/what.html
546
+ # HEIC is composed of nested "boxes". Each box has a header composed of
547
+ # - Size (32 bit integer)
548
+ # - Box type (4 chars)
549
+ # - Extended size: only if size === 1, the type field is followed by 64 bit integer of extended size
550
+ # - Payload: Type-dependent
551
+ case @stream.peek(12)[4..-1]
552
+ when "ftypheic"
553
+ :heic
554
+ when "ftypmif1"
555
+ :heif
556
+ end
557
+ # ico has either a 1 (for ico format) or 2 (for cursor) at offset 3
499
558
  when 1 then :ico
500
559
  when 2 then :cur
501
560
  end
502
561
  when "RI"
503
562
  :webp if @stream.peek(12)[8..11] == "WEBP"
504
563
  when "<s"
505
- :svg
506
- when /<[?!]/
564
+ :svg if @stream.peek(4) == "<svg"
565
+ when /\s\s|\s<|<[?!]/, 0xef.chr + 0xbb.chr
507
566
  # Peek 10 more chars each time, and if end of file is reached just raise
508
567
  # unknown. We assume the <svg tag cannot be within 10 chars of the end of
509
568
  # the file, and is within the first 250 chars.
@@ -524,8 +583,222 @@ class FastImage
524
583
  end
525
584
  alias_method :parse_size_for_cur, :parse_size_for_ico
526
585
 
586
+ class Heic # :nodoc:
587
+ def initialize(stream)
588
+ @stream = stream
589
+ end
590
+
591
+ def width_and_height
592
+ @max_size = nil
593
+ @primary_box = nil
594
+ @ipma_boxes = []
595
+ @ispe_boxes = []
596
+ @final_size = nil
597
+
598
+ catch :finish do
599
+ read_boxes!
600
+ end
601
+
602
+ @final_size
603
+ end
604
+
605
+ private
606
+
607
+ def read_boxes!(max_read_bytes = nil)
608
+ end_pos = max_read_bytes.nil? ? nil : @stream.pos + max_read_bytes
609
+ index = 0
610
+
611
+ loop do
612
+ return if end_pos && @stream.pos >= end_pos
613
+
614
+ box_type, box_size = read_box_header!
615
+
616
+ case box_type
617
+ when "meta"
618
+ handle_meta_box(box_size)
619
+ when "pitm"
620
+ handle_pitm_box(box_size)
621
+ when "ipma"
622
+ handle_ipma_box(box_size)
623
+ when "hdlr"
624
+ handle_hdlr_box(box_size)
625
+ when "iprp", "ipco"
626
+ read_boxes!(box_size)
627
+ when "ispe"
628
+ handle_ispe_box(box_size, index)
629
+ when "mdat"
630
+ throw :finish
631
+ else
632
+ @stream.read(box_size)
633
+ end
634
+
635
+ index += 1
636
+ end
637
+ end
638
+
639
+ def handle_ispe_box(box_size, index)
640
+ throw :finish if box_size < 12
641
+
642
+ data = @stream.read(box_size)
643
+ width, height = data[4...12].unpack("N2")
644
+ @ispe_boxes << { index: index, size: [width, height] }
645
+ end
646
+
647
+ def handle_hdlr_box(box_size)
648
+ throw :finish if box_size < 12
649
+
650
+ data = @stream.read(box_size)
651
+ throw :finish if data[8...12] != "pict"
652
+ end
653
+
654
+ def handle_ipma_box(box_size)
655
+ @stream.read(3)
656
+ flags3 = read_uint8!
657
+ entries_count = read_uint32!
658
+
659
+ entries_count.times do
660
+ id = read_uint16!
661
+ essen_count = read_uint8!
662
+
663
+ essen_count.times do
664
+ property_index = read_uint8! & 0x7F
665
+
666
+ if flags3 & 1 == 1
667
+ property_index = (property_index << 7) + read_uint8!
668
+ end
669
+
670
+ @ipma_boxes << { id: id, property_index: property_index - 1 }
671
+ end
672
+ end
673
+ end
674
+
675
+ def handle_pitm_box(box_size)
676
+ data = @stream.read(box_size)
677
+ @primary_box = data[4...6].unpack("S>")[0]
678
+ end
679
+
680
+ def handle_meta_box(box_size)
681
+ throw :finish if box_size < 4
682
+
683
+ @stream.read(4)
684
+ read_boxes!(box_size - 4)
685
+
686
+ throw :finish if !@primary_box
687
+
688
+ primary_indices = @ipma_boxes
689
+ .select { |box| box[:id] == @primary_box }
690
+ .map { |box| box[:property_index] }
691
+
692
+ ispe_box = @ispe_boxes.find do |box|
693
+ primary_indices.include?(box[:index])
694
+ end
695
+
696
+ if ispe_box
697
+ @final_size = ispe_box[:size]
698
+ end
699
+
700
+ throw :finish
701
+ end
702
+
703
+ def read_box_header!
704
+ size = read_uint32!
705
+ type = @stream.read(4)
706
+ [type, size - 8]
707
+ end
708
+
709
+ def read_uint8!
710
+ @stream.read(1).unpack("C")[0]
711
+ end
712
+
713
+ def read_uint16!
714
+ @stream.read(2).unpack("S>")[0]
715
+ end
716
+
717
+ def read_uint32!
718
+ @stream.read(4).unpack("N")[0]
719
+ end
720
+ end
721
+
722
+ def parse_size_for_heic
723
+ heic = Heic.new(@stream)
724
+ heic.width_and_height
725
+ end
726
+
727
+ def parse_size_for_heif
728
+ heic = Heic.new(@stream)
729
+ heic.width_and_height
730
+ end
731
+
732
+ class Gif # :nodoc:
733
+ def initialize(stream)
734
+ @stream = stream
735
+ end
736
+
737
+ def width_and_height
738
+ @stream.read(11)[6..10].unpack('SS')
739
+ end
740
+
741
+ # Checks if a delay between frames exists and if it does, then the GIFs is
742
+ # animated
743
+ def animated?
744
+ frames = 0
745
+
746
+ # "GIF" + version (3) + width (2) + height (2)
747
+ @stream.skip(10)
748
+
749
+ # fields (1) + bg color (1) + pixel ratio (1)
750
+ fields = @stream.read(3).unpack("CCC")[0]
751
+ if fields & 0x80 != 0 # Global Color Table
752
+ # 2 * (depth + 1) colors, each occupying 3 bytes (RGB)
753
+ @stream.skip(3 * 2 ** ((fields & 0x7) + 1))
754
+ end
755
+
756
+ loop do
757
+ block_type = @stream.read(1).unpack("C")[0]
758
+
759
+ if block_type == 0x21 # Graphic Control Extension
760
+ # extension type (1) + size (1)
761
+ size = @stream.read(2).unpack("CC")[1]
762
+ @stream.skip(size)
763
+ skip_sub_blocks
764
+ elsif block_type == 0x2C # Image Descriptor
765
+ frames += 1
766
+ return true if frames > 1
767
+
768
+ # left position (2) + top position (2) + width (2) + height (2) + fields (1)
769
+ fields = @stream.read(9).unpack("SSSSC")[4]
770
+ if fields & 0x80 != 0 # Local Color Table
771
+ # 2 * (depth + 1) colors, each occupying 3 bytes (RGB)
772
+ @stream.skip(3 * 2 ** ((fields & 0x7) + 1))
773
+ end
774
+
775
+ @stream.skip(1) # LZW min code size (1)
776
+ skip_sub_blocks
777
+ else
778
+ break # unrecognized block
779
+ end
780
+ end
781
+
782
+ false
783
+ end
784
+
785
+ private
786
+
787
+ def skip_sub_blocks
788
+ loop do
789
+ size = @stream.read(1).unpack("C")[0]
790
+ if size == 0
791
+ break
792
+ else
793
+ @stream.skip(size)
794
+ end
795
+ end
796
+ end
797
+ end
798
+
527
799
  def parse_size_for_gif
528
- @stream.read(11)[6..10].unpack('SS')
800
+ gif = Gif.new(@stream)
801
+ gif.width_and_height
529
802
  end
530
803
 
531
804
  def parse_size_for_png
@@ -785,4 +1058,9 @@ class FastImage
785
1058
  svg = Svg.new(@stream)
786
1059
  svg.width_and_height
787
1060
  end
1061
+
1062
+ def parse_animated_for_gif
1063
+ gif = Gif.new(@stream)
1064
+ gif.animated?
1065
+ end
788
1066
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastimage
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.7
4
+ version: 2.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Sykes
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-09-05 00:00:00.000000000 Z
11
+ date: 2021-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fakeweb-fi
@@ -28,14 +28,14 @@ dependencies:
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '10.5'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '10.5'
41
41
  - !ruby/object:Gem::Dependency
@@ -81,7 +81,7 @@ homepage: http://github.com/sdsykes/fastimage
81
81
  licenses:
82
82
  - MIT
83
83
  metadata: {}
84
- post_install_message:
84
+ post_install_message:
85
85
  rdoc_options:
86
86
  - "--charset=UTF-8"
87
87
  require_paths:
@@ -97,8 +97,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
97
  - !ruby/object:Gem::Version
98
98
  version: '0'
99
99
  requirements: []
100
- rubygems_version: 3.0.6
101
- signing_key:
100
+ rubygems_version: 3.1.6
101
+ signing_key:
102
102
  specification_version: 4
103
103
  summary: FastImage - Image info fast
104
104
  test_files: []