fastimage 2.1.7 → 2.2.6

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2aaa7bc9e7b596326f39fc933d836e8d94a0c19601670c0b55c839ba8d0e9ed5
4
- data.tar.gz: b807e4cb3ac6ac16b10c026d15ac6a6440a65a75036482518b1a337475bd723b
3
+ metadata.gz: d7667683a4bbc8ac16d583fb3ed7c93e6a58a25c19bf8df909ab7dae8d4ef782
4
+ data.tar.gz: 0baf5dcf6af104d0edd38baf65b7b35c972e6fc4271363d52855071e22b97a82
5
5
  SHA512:
6
- metadata.gz: 8a87fd1e5eb5a209da0c2a2af1d7c0d6cead3f14af8cc591810e0e0d7b853cd519e236f9ec0ed70fdea0c39569631f5b41f88f5fea8942aa55fbe018390102ef
7
- data.tar.gz: 002d20c88557352e4b3ebb80975d2f26cffa221cc3c510a0865ffed55fe251a63f53dfeff53843f351574cff1aa9c9864d5df1a2e5fc9a4550b5e03fd798880a
6
+ metadata.gz: 9261157ade925ba5be4761028b4d70a698f1cc4d54e4ef0fcb7f4b23b6120ae6f9c42516b0dc17f915c423d168a4ef66c805a686ea20e3ca311556ce3e930593
7
+ data.tar.gz: 2d319a0aa6c4e7c7a3d951b3025d6215ac999bc2efe5d054720dac9c063cb3e2939e35e30660cf360bfae5f88285b91bb8029ec594c64ca1eb9e3e4144fa6b4c
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
 
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FastImage
4
+ VERSION = '2.2.6'
5
+ end
data/lib/fastimage.rb CHANGED
@@ -61,6 +61,7 @@ require 'pathname'
61
61
  require 'zlib'
62
62
  require 'base64'
63
63
  require 'uri'
64
+ require_relative 'fastimage/version'
64
65
 
65
66
  # see http://stackoverflow.com/questions/5208851/i/41048816#41048816
66
67
  if RUBY_VERSION < "2.2"
@@ -70,7 +71,7 @@ if RUBY_VERSION < "2.2"
70
71
  end
71
72
 
72
73
  class FastImage
73
- attr_reader :size, :type, :content_length, :orientation
74
+ attr_reader :size, :type, :content_length, :orientation, :animated
74
75
 
75
76
  attr_reader :bytes_read
76
77
 
@@ -89,6 +90,8 @@ class FastImage
89
90
 
90
91
  LocalFileChunkSize = 256 unless const_defined?(:LocalFileChunkSize)
91
92
 
93
+ SUPPORTED_IMAGE_TYPES = [:bmp, :gif, :jpeg, :png, :tiff, :psd, :heic, :heif, :webp, :svg, :ico, :cur].freeze
94
+
92
95
  # Returns an array containing the width and height of the image.
93
96
  # It will return nil if the image could not be fetched, or if the image type was not recognised.
94
97
  #
@@ -179,6 +182,34 @@ class FastImage
179
182
  new(uri, options.merge(:type_only=>true)).type
180
183
  end
181
184
 
185
+ # Returns a boolean value indicating the image is animated.
186
+ # It will return nil if the image could not be fetched, or if the image type was not recognised.
187
+ #
188
+ # By default there is a timeout of 2 seconds for opening and reading from a remote server.
189
+ # This can be changed by passing a :timeout => number_of_seconds in the options.
190
+ #
191
+ # If you wish FastImage to raise if it cannot find the type of the image for any reason, then pass
192
+ # :raise_on_failure => true in the options.
193
+ #
194
+ # === Example
195
+ #
196
+ # require 'fastimage'
197
+ #
198
+ # FastImage.animated?("test/fixtures/test.gif")
199
+ # => false
200
+ # FastImage.animated?("test/fixtures/animated.gif")
201
+ # => true
202
+ #
203
+ # === Supported options
204
+ # [:timeout]
205
+ # Overrides the default timeout of 2 seconds. Applies both to reading from and opening the http connection.
206
+ # [:raise_on_failure]
207
+ # If set to true causes an exception to be raised if the image type cannot be found for any reason.
208
+ #
209
+ def self.animated?(uri, options={})
210
+ new(uri, options.merge(:animated_only=>true)).animated
211
+ end
212
+
182
213
  def initialize(uri, options={})
183
214
  @uri = uri
184
215
  @options = {
@@ -189,7 +220,13 @@ class FastImage
189
220
  :http_header => {}
190
221
  }.merge(options)
191
222
 
192
- @property = @options[:type_only] ? :type : :size
223
+ @property = if @options[:animated_only]
224
+ :animated
225
+ elsif @options[:type_only]
226
+ :type
227
+ else
228
+ :size
229
+ end
193
230
 
194
231
  @type, @state = nil
195
232
 
@@ -217,10 +254,8 @@ class FastImage
217
254
  Errno::ENETUNREACH, ImageFetchFailure, Net::HTTPBadResponse, EOFError, Errno::ENOENT,
218
255
  OpenSSL::SSL::SSLError
219
256
  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
257
  rescue UnknownImageType
223
- raise UnknownImageType if @options[:raise_on_failure]
258
+ raise if @options[:raise_on_failure]
224
259
  rescue CannotParseImage
225
260
  if @options[:raise_on_failure]
226
261
  if @property == :size
@@ -248,7 +283,7 @@ class FastImage
248
283
  begin
249
284
  URI(location)
250
285
  rescue URI::InvalidURIError
251
- URI.escape(location)
286
+ ::URI::DEFAULT_PARSER.escape(location)
252
287
  else
253
288
  location
254
289
  end
@@ -262,7 +297,10 @@ class FastImage
262
297
  if res.is_a?(Net::HTTPRedirection) && @redirect_count < 4
263
298
  @redirect_count += 1
264
299
  begin
265
- @parsed_uri = URI.join(@parsed_uri, escaped_location(res['Location']))
300
+ location = res['Location']
301
+ raise ImageFetchFailure if location.nil? || location.empty?
302
+
303
+ @parsed_uri = URI.join(@parsed_uri, escaped_location(location))
266
304
  rescue URI::InvalidURIError
267
305
  else
268
306
  fetch_using_http_from_parsed_uri
@@ -369,7 +407,7 @@ class FastImage
369
407
 
370
408
  begin
371
409
  result = send("parse_#{@property}")
372
- if result
410
+ if result != nil
373
411
  # extract exif orientation if it was found
374
412
  if @property == :size && result.size == 3
375
413
  @orientation = result.pop
@@ -391,9 +429,17 @@ class FastImage
391
429
  send("parse_size_for_#{@type}")
392
430
  end
393
431
 
432
+ def parse_animated
433
+ @type = parse_type unless @type
434
+ @type == :gif ? send("parse_animated_for_#{@type}") : nil
435
+ end
436
+
394
437
  def fetch_using_base64(uri)
395
- data = uri.split(',')[1]
396
- decoded = Base64.decode64(data)
438
+ decoded = begin
439
+ Base64.decode64(uri.split(',')[1])
440
+ rescue
441
+ raise CannotParseImage
442
+ end
397
443
  @content_length = decoded.size
398
444
  fetch_using_read StringIO.new(decoded)
399
445
  end
@@ -429,19 +475,20 @@ class FastImage
429
475
 
430
476
  # Peeking beyond the end of the input will raise
431
477
  def peek(n)
432
- while @strpos + n - 1 >= @str.size
478
+ while @strpos + n > @str.size
433
479
  unused_str = @str[@strpos..-1]
480
+
434
481
  new_string = @read_fiber.resume
482
+ new_string = @read_fiber.resume if new_string.is_a? Net::ReadAdapter
435
483
  raise CannotParseImage if !new_string
436
-
437
484
  # we are dealing with bytes here, so force the encoding
438
- new_string.force_encoding("ASCII-8BIT") if String.method_defined? :force_encoding
485
+ new_string.force_encoding("ASCII-8BIT") if new_string.respond_to? :force_encoding
439
486
 
440
487
  @str = unused_str + new_string
441
488
  @strpos = 0
442
489
  end
443
490
 
444
- @str[@strpos..(@strpos + n - 1)]
491
+ @str[@strpos, n]
445
492
  end
446
493
 
447
494
  def read(n)
@@ -459,7 +506,7 @@ class FastImage
459
506
  new_string = @read_fiber.resume
460
507
  raise CannotParseImage if !new_string
461
508
 
462
- new_string.force_encoding("ASCII-8BIT") if String.method_defined? :force_encoding
509
+ new_string.force_encoding("ASCII-8BIT") if new_string.respond_to? :force_encoding
463
510
 
464
511
  fetched += new_string.size
465
512
  @str = new_string
@@ -494,16 +541,26 @@ class FastImage
494
541
  when '8B'
495
542
  :psd
496
543
  when "\0\0"
497
- # ico has either a 1 (for ico format) or 2 (for cursor) at offset 3
498
544
  case @stream.peek(3).bytes.to_a.last
545
+ when 0
546
+ # http://www.ftyps.com/what.html
547
+ case @stream.peek(12)[4..-1]
548
+ when "ftypavif"
549
+ :avif
550
+ when "ftypheic"
551
+ :heic
552
+ when "ftypmif1"
553
+ :heif
554
+ end
555
+ # ico has either a 1 (for ico format) or 2 (for cursor) at offset 3
499
556
  when 1 then :ico
500
557
  when 2 then :cur
501
558
  end
502
559
  when "RI"
503
560
  :webp if @stream.peek(12)[8..11] == "WEBP"
504
561
  when "<s"
505
- :svg
506
- when /<[?!]/
562
+ :svg if @stream.peek(4) == "<svg"
563
+ when /\s\s|\s<|<[?!]/, 0xef.chr + 0xbb.chr
507
564
  # Peek 10 more chars each time, and if end of file is reached just raise
508
565
  # unknown. We assume the <svg tag cannot be within 10 chars of the end of
509
566
  # the file, and is within the first 250 chars.
@@ -524,8 +581,249 @@ class FastImage
524
581
  end
525
582
  alias_method :parse_size_for_cur, :parse_size_for_ico
526
583
 
584
+ # HEIC/AVIF are a special case of the general ISO_BMFF format, in which all data is encapsulated in typed boxes,
585
+ # with a mandatory ftyp box that is used to indicate particular file types. Is composed of nested "boxes". Each
586
+ # box has a header composed of
587
+ # - Size (32 bit integer)
588
+ # - Box type (4 chars)
589
+ # - Extended size: only if size === 1, the type field is followed by 64 bit integer of extended size
590
+ # - Payload: Type-dependent
591
+ class IsoBmff # :nodoc:
592
+ def initialize(stream)
593
+ @stream = stream
594
+ end
595
+
596
+ def width_and_height
597
+ @rotation = 0
598
+ @max_size = nil
599
+ @primary_box = nil
600
+ @ipma_boxes = []
601
+ @ispe_boxes = []
602
+ @final_size = nil
603
+
604
+ catch :finish do
605
+ read_boxes!
606
+ end
607
+
608
+ if [90, 270].include?(@rotation)
609
+ @final_size.reverse
610
+ else
611
+ @final_size
612
+ end
613
+ end
614
+
615
+ private
616
+
617
+ # Format specs: https://www.loc.gov/preservation/digital/formats/fdd/fdd000525.shtml
618
+
619
+ # If you need to inspect a heic/heif file, use
620
+ # https://gpac.github.io/mp4box.js/test/filereader.html
621
+ def read_boxes!(max_read_bytes = nil)
622
+ end_pos = max_read_bytes.nil? ? nil : @stream.pos + max_read_bytes
623
+ index = 0
624
+
625
+ loop do
626
+ return if end_pos && @stream.pos >= end_pos
627
+
628
+ box_type, box_size = read_box_header!
629
+
630
+ case box_type
631
+ when "meta"
632
+ handle_meta_box(box_size)
633
+ when "pitm"
634
+ handle_pitm_box(box_size)
635
+ when "ipma"
636
+ handle_ipma_box(box_size)
637
+ when "hdlr"
638
+ handle_hdlr_box(box_size)
639
+ when "iprp", "ipco"
640
+ read_boxes!(box_size)
641
+ when "irot"
642
+ handle_irot_box
643
+ when "ispe"
644
+ handle_ispe_box(box_size, index)
645
+ when "mdat"
646
+ throw :finish
647
+ else
648
+ @stream.read(box_size)
649
+ end
650
+
651
+ index += 1
652
+ end
653
+ end
654
+
655
+ def handle_irot_box
656
+ @rotation = (read_uint8! & 0x3) * 90
657
+ end
658
+
659
+ def handle_ispe_box(box_size, index)
660
+ throw :finish if box_size < 12
661
+
662
+ data = @stream.read(box_size)
663
+ width, height = data[4...12].unpack("N2")
664
+ @ispe_boxes << { index: index, size: [width, height] }
665
+ end
666
+
667
+ def handle_hdlr_box(box_size)
668
+ throw :finish if box_size < 12
669
+
670
+ data = @stream.read(box_size)
671
+ throw :finish if data[8...12] != "pict"
672
+ end
673
+
674
+ def handle_ipma_box(box_size)
675
+ @stream.read(3)
676
+ flags3 = read_uint8!
677
+ entries_count = read_uint32!
678
+
679
+ entries_count.times do
680
+ id = read_uint16!
681
+ essen_count = read_uint8!
682
+
683
+ essen_count.times do
684
+ property_index = read_uint8! & 0x7F
685
+
686
+ if flags3 & 1 == 1
687
+ property_index = (property_index << 7) + read_uint8!
688
+ end
689
+
690
+ @ipma_boxes << { id: id, property_index: property_index - 1 }
691
+ end
692
+ end
693
+ end
694
+
695
+ def handle_pitm_box(box_size)
696
+ data = @stream.read(box_size)
697
+ @primary_box = data[4...6].unpack("S>")[0]
698
+ end
699
+
700
+ def handle_meta_box(box_size)
701
+ throw :finish if box_size < 4
702
+
703
+ @stream.read(4)
704
+ read_boxes!(box_size - 4)
705
+
706
+ throw :finish if !@primary_box
707
+
708
+ primary_indices = @ipma_boxes
709
+ .select { |box| box[:id] == @primary_box }
710
+ .map { |box| box[:property_index] }
711
+
712
+ ispe_box = @ispe_boxes.find do |box|
713
+ primary_indices.include?(box[:index])
714
+ end
715
+
716
+ if ispe_box
717
+ @final_size = ispe_box[:size]
718
+ end
719
+
720
+ throw :finish
721
+ end
722
+
723
+ def read_box_header!
724
+ size = read_uint32!
725
+ type = @stream.read(4)
726
+ [type, size - 8]
727
+ end
728
+
729
+ def read_uint8!
730
+ @stream.read(1).unpack("C")[0]
731
+ end
732
+
733
+ def read_uint16!
734
+ @stream.read(2).unpack("S>")[0]
735
+ end
736
+
737
+ def read_uint32!
738
+ @stream.read(4).unpack("N")[0]
739
+ end
740
+ end
741
+
742
+ def parse_size_for_avif
743
+ bmff = IsoBmff.new(@stream)
744
+ bmff.width_and_height
745
+ end
746
+
747
+ def parse_size_for_heic
748
+ bmff = IsoBmff.new(@stream)
749
+ bmff.width_and_height
750
+ end
751
+
752
+ def parse_size_for_heif
753
+ bmff = IsoBmff.new(@stream)
754
+ bmff.width_and_height
755
+ end
756
+
757
+ class Gif # :nodoc:
758
+ def initialize(stream)
759
+ @stream = stream
760
+ end
761
+
762
+ def width_and_height
763
+ @stream.read(11)[6..10].unpack('SS')
764
+ end
765
+
766
+ # Checks if a delay between frames exists and if it does, then the GIFs is
767
+ # animated
768
+ def animated?
769
+ frames = 0
770
+
771
+ # "GIF" + version (3) + width (2) + height (2)
772
+ @stream.skip(10)
773
+
774
+ # fields (1) + bg color (1) + pixel ratio (1)
775
+ fields = @stream.read(3).unpack("CCC")[0]
776
+ if fields & 0x80 != 0 # Global Color Table
777
+ # 2 * (depth + 1) colors, each occupying 3 bytes (RGB)
778
+ @stream.skip(3 * 2 ** ((fields & 0x7) + 1))
779
+ end
780
+
781
+ loop do
782
+ block_type = @stream.read(1).unpack("C")[0]
783
+
784
+ if block_type == 0x21 # Graphic Control Extension
785
+ # extension type (1) + size (1)
786
+ size = @stream.read(2).unpack("CC")[1]
787
+ @stream.skip(size)
788
+ skip_sub_blocks
789
+ elsif block_type == 0x2C # Image Descriptor
790
+ frames += 1
791
+ return true if frames > 1
792
+
793
+ # left position (2) + top position (2) + width (2) + height (2) + fields (1)
794
+ fields = @stream.read(9).unpack("SSSSC")[4]
795
+ if fields & 0x80 != 0 # Local Color Table
796
+ # 2 * (depth + 1) colors, each occupying 3 bytes (RGB)
797
+ @stream.skip(3 * 2 ** ((fields & 0x7) + 1))
798
+ end
799
+
800
+ @stream.skip(1) # LZW min code size (1)
801
+ skip_sub_blocks
802
+ else
803
+ break # unrecognized block
804
+ end
805
+ end
806
+
807
+ false
808
+ end
809
+
810
+ private
811
+
812
+ def skip_sub_blocks
813
+ loop do
814
+ size = @stream.read(1).unpack("C")[0]
815
+ if size == 0
816
+ break
817
+ else
818
+ @stream.skip(size)
819
+ end
820
+ end
821
+ end
822
+ end
823
+
527
824
  def parse_size_for_gif
528
- @stream.read(11)[6..10].unpack('SS')
825
+ gif = Gif.new(@stream)
826
+ gif.width_and_height
529
827
  end
530
828
 
531
829
  def parse_size_for_png
@@ -785,4 +1083,9 @@ class FastImage
785
1083
  svg = Svg.new(@stream)
786
1084
  svg.width_and_height
787
1085
  end
1086
+
1087
+ def parse_animated_for_gif
1088
+ gif = Gif.new(@stream)
1089
+ gif.animated?
1090
+ end
788
1091
  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.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Sykes
8
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
@@ -77,6 +77,7 @@ files:
77
77
  - MIT-LICENSE
78
78
  - README.textile
79
79
  - lib/fastimage.rb
80
+ - lib/fastimage/version.rb
80
81
  homepage: http://github.com/sdsykes/fastimage
81
82
  licenses:
82
83
  - MIT
@@ -97,7 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
98
  - !ruby/object:Gem::Version
98
99
  version: '0'
99
100
  requirements: []
100
- rubygems_version: 3.0.6
101
+ rubygems_version: 3.1.6
101
102
  signing_key:
102
103
  specification_version: 4
103
104
  summary: FastImage - Image info fast