fastimage 2.3.1 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,63 @@
1
+ module FastImageParsing
2
+ class Gif < ImageBase # :nodoc:
3
+ def dimensions
4
+ @stream.read(11)[6..10].unpack('SS')
5
+ end
6
+
7
+ # Checks for multiple frames
8
+ def animated?
9
+ frames = 0
10
+
11
+ # "GIF" + version (3) + width (2) + height (2)
12
+ @stream.skip(10)
13
+
14
+ # fields (1) + bg color (1) + pixel ratio (1)
15
+ fields = @stream.read(3).unpack("CCC")[0]
16
+ if fields & 0x80 != 0 # Global Color Table
17
+ # 2 * (depth + 1) colors, each occupying 3 bytes (RGB)
18
+ @stream.skip(3 * 2 ** ((fields & 0x7) + 1))
19
+ end
20
+
21
+ loop do
22
+ block_type = @stream.read(1).unpack("C")[0]
23
+
24
+ if block_type == 0x21 # Graphic Control Extension
25
+ # extension type (1) + size (1)
26
+ size = @stream.read(2).unpack("CC")[1]
27
+ @stream.skip(size)
28
+ skip_sub_blocks
29
+ elsif block_type == 0x2C # Image Descriptor
30
+ frames += 1
31
+ return true if frames > 1
32
+
33
+ # left position (2) + top position (2) + width (2) + height (2) + fields (1)
34
+ fields = @stream.read(9).unpack("SSSSC")[4]
35
+ if fields & 0x80 != 0 # Local Color Table
36
+ # 2 * (depth + 1) colors, each occupying 3 bytes (RGB)
37
+ @stream.skip(3 * 2 ** ((fields & 0x7) + 1))
38
+ end
39
+
40
+ @stream.skip(1) # LZW min code size (1)
41
+ skip_sub_blocks
42
+ else
43
+ break # unrecognized block
44
+ end
45
+ end
46
+
47
+ false
48
+ end
49
+
50
+ private
51
+
52
+ def skip_sub_blocks
53
+ loop do
54
+ size = @stream.read(1).unpack("C")[0]
55
+ if size == 0
56
+ break
57
+ else
58
+ @stream.skip(size)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,8 @@
1
+ module FastImageParsing
2
+ class Heic < ImageBase # :nodoc:
3
+ def dimensions
4
+ bmff = IsoBmff.new(@stream)
5
+ [bmff.width, bmff.height]
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ module FastImageParsing
2
+ class Ico < ImageBase
3
+ def dimensions
4
+ icons = @stream.read(6)[4..5].unpack('v').first
5
+ sizes = icons.times.map { @stream.read(16).unpack('C2').map { |x| x == 0 ? 256 : x } }.sort_by { |w,h| w * h }
6
+ sizes.last
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ module FastImageParsing
2
+ class ImageBase # :nodoc:
3
+ def initialize(stream)
4
+ @stream = stream
5
+ end
6
+
7
+ # Implement in subclasses
8
+ def dimensions
9
+ raise NotImplementedError
10
+ end
11
+
12
+ # Implement in subclasses if appropriate
13
+ def animated?
14
+ nil
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,176 @@
1
+ module FastImageParsing
2
+ # HEIC/AVIF are a special case of the general ISO_BMFF format, in which all data is encapsulated in typed boxes,
3
+ # with a mandatory ftyp box that is used to indicate particular file types. Is composed of nested "boxes". Each
4
+ # box has a header composed of
5
+ # - Size (32 bit integer)
6
+ # - Box type (4 chars)
7
+ # - Extended size: only if size === 1, the type field is followed by 64 bit integer of extended size
8
+ # - Payload: Type-dependent
9
+ class IsoBmff # :nodoc:
10
+ attr_reader :width, :height
11
+
12
+ def initialize(stream)
13
+ @stream = stream
14
+ @width, @height = nil
15
+ parse_isobmff
16
+ end
17
+
18
+ def parse_isobmff
19
+ @rotation = 0
20
+ @max_size = nil
21
+ @primary_box = nil
22
+ @ipma_boxes = []
23
+ @ispe_boxes = []
24
+ @final_size = nil
25
+
26
+ catch :finish do
27
+ read_boxes!
28
+ end
29
+
30
+ if [90, 270].include?(@rotation)
31
+ @final_size.reverse!
32
+ end
33
+
34
+ @width, @height = @final_size
35
+ end
36
+
37
+ private
38
+
39
+ # Format specs: https://www.loc.gov/preservation/digital/formats/fdd/fdd000525.shtml
40
+
41
+ # If you need to inspect a heic/heif file, use
42
+ # https://gpac.github.io/mp4box.js/test/filereader.html
43
+ def read_boxes!(max_read_bytes = nil)
44
+ end_pos = max_read_bytes.nil? ? nil : @stream.pos + max_read_bytes
45
+ index = 0
46
+
47
+ loop do
48
+ return if end_pos && @stream.pos >= end_pos
49
+
50
+ box_type, box_size = read_box_header!
51
+
52
+ case box_type
53
+ when "meta"
54
+ handle_meta_box(box_size)
55
+ when "pitm"
56
+ handle_pitm_box(box_size)
57
+ when "ipma"
58
+ handle_ipma_box(box_size)
59
+ when "hdlr"
60
+ handle_hdlr_box(box_size)
61
+ when "iprp", "ipco"
62
+ read_boxes!(box_size)
63
+ when "irot"
64
+ handle_irot_box
65
+ when "ispe"
66
+ handle_ispe_box(box_size, index)
67
+ when "mdat"
68
+ @stream.skip(box_size)
69
+ when "jxlc"
70
+ handle_jxlc_box(box_size)
71
+ else
72
+ @stream.skip(box_size)
73
+ end
74
+
75
+ index += 1
76
+ end
77
+ end
78
+
79
+ def handle_irot_box
80
+ @rotation = (read_uint8! & 0x3) * 90
81
+ end
82
+
83
+ def handle_ispe_box(box_size, index)
84
+ throw :finish if box_size < 12
85
+
86
+ data = @stream.read(box_size)
87
+ width, height = data[4...12].unpack("N2")
88
+ @ispe_boxes << { index: index, size: [width, height] }
89
+ end
90
+
91
+ def handle_hdlr_box(box_size)
92
+ throw :finish if box_size < 12
93
+
94
+ data = @stream.read(box_size)
95
+ throw :finish if data[8...12] != "pict"
96
+ end
97
+
98
+ def handle_ipma_box(box_size)
99
+ @stream.read(3)
100
+ flags3 = read_uint8!
101
+ entries_count = read_uint32!
102
+
103
+ entries_count.times do
104
+ id = read_uint16!
105
+ essen_count = read_uint8!
106
+
107
+ essen_count.times do
108
+ property_index = read_uint8! & 0x7F
109
+
110
+ if flags3 & 1 == 1
111
+ property_index = (property_index << 7) + read_uint8!
112
+ end
113
+
114
+ @ipma_boxes << { id: id, property_index: property_index - 1 }
115
+ end
116
+ end
117
+ end
118
+
119
+ def handle_pitm_box(box_size)
120
+ data = @stream.read(box_size)
121
+ @primary_box = data[4...6].unpack("S>")[0]
122
+ end
123
+
124
+ def handle_meta_box(box_size)
125
+ throw :finish if box_size < 4
126
+
127
+ @stream.read(4)
128
+ read_boxes!(box_size - 4)
129
+
130
+ throw :finish if !@primary_box
131
+
132
+ primary_indices = @ipma_boxes
133
+ .select { |box| box[:id] == @primary_box }
134
+ .map { |box| box[:property_index] }
135
+
136
+ ispe_box = @ispe_boxes.find do |box|
137
+ primary_indices.include?(box[:index])
138
+ end
139
+
140
+ if ispe_box
141
+ @final_size = ispe_box[:size]
142
+ end
143
+
144
+ throw :finish
145
+ end
146
+
147
+ def handle_jxlc_box(box_size)
148
+ jxlc = Jxlc.new(@stream)
149
+ @final_size = [jxlc.width, jxlc.height]
150
+ throw :finish
151
+ end
152
+
153
+ def read_box_header!
154
+ size = read_uint32!
155
+ type = @stream.read(4)
156
+ size = read_uint64! - 8 if size == 1
157
+ [type, size - 8]
158
+ end
159
+
160
+ def read_uint8!
161
+ @stream.read(1).unpack("C")[0]
162
+ end
163
+
164
+ def read_uint16!
165
+ @stream.read(2).unpack("S>")[0]
166
+ end
167
+
168
+ def read_uint32!
169
+ @stream.read(4).unpack("N")[0]
170
+ end
171
+
172
+ def read_uint64!
173
+ @stream.read(8).unpack("Q>")[0]
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,52 @@
1
+ module FastImageParsing
2
+ class IOStream < SimpleDelegator # :nodoc:
3
+ include StreamUtil
4
+ end
5
+
6
+ class Jpeg < ImageBase # :nodoc:
7
+ def dimensions
8
+ exif = nil
9
+ state = nil
10
+ loop do
11
+ state = case state
12
+ when nil
13
+ @stream.skip(2)
14
+ :started
15
+ when :started
16
+ @stream.read_byte == 0xFF ? :sof : :started
17
+ when :sof
18
+ case @stream.read_byte
19
+ when 0xe1 # APP1
20
+ skip_chars = @stream.read_int - 2
21
+ data = @stream.read(skip_chars)
22
+ io = StringIO.new(data)
23
+ if io.read(4) == "Exif"
24
+ io.read(2)
25
+ new_exif = Exif.new(IOStream.new(io)) rescue nil
26
+ exif ||= new_exif # only use the first APP1 segment
27
+ end
28
+ :started
29
+ when 0xe0..0xef
30
+ :skipframe
31
+ when 0xC0..0xC3, 0xC5..0xC7, 0xC9..0xCB, 0xCD..0xCF
32
+ :readsize
33
+ when 0xFF
34
+ :sof
35
+ else
36
+ :skipframe
37
+ end
38
+ when :skipframe
39
+ skip_chars = @stream.read_int - 2
40
+ @stream.skip(skip_chars)
41
+ :started
42
+ when :readsize
43
+ @stream.skip(3)
44
+ height = @stream.read_int
45
+ width = @stream.read_int
46
+ width, height = height, width if exif && exif.rotated?
47
+ return [width, height, exif ? exif.orientation : 1]
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,13 @@
1
+ module FastImageParsing
2
+ class Jxl < ImageBase # :nodoc:
3
+ def dimensions
4
+ if @stream.peek(2) == "\xFF\x0A".b
5
+ jxlc = Jxlc.new(@stream)
6
+ [jxlc.width, jxlc.height]
7
+ else
8
+ bmff = IsoBmff.new(@stream)
9
+ [bmff.width, bmff.height]
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,75 @@
1
+ module FastImageParsing
2
+ class Jxlc # :nodoc:
3
+ attr_reader :width, :height
4
+
5
+ LENGTHS = [9, 13, 18, 30]
6
+ MULTIPLIERS = [1, 1.2, Rational(4, 3), 1.5, Rational(16, 9), 1.25, 2]
7
+
8
+ def initialize(stream)
9
+ @stream = stream
10
+ @width, @height´ = nil
11
+ @bit_counter = 0
12
+ parse_jxlc
13
+ end
14
+
15
+ def parse_jxlc
16
+ @words = @stream.read(6)[2..5].unpack('vv')
17
+
18
+ # small mode allows for values <= 256 that are divisible by 8
19
+ small = get_bits(1)
20
+ if small == 1
21
+ y = (get_bits(5) + 1) * 8
22
+ x = x_from_ratio(y)
23
+ if !x
24
+ x = (get_bits(5) + 1) * 8
25
+ end
26
+ @width, @height = x, y
27
+ return
28
+ end
29
+
30
+ len = LENGTHS[get_bits(2)]
31
+ y = get_bits(len) + 1
32
+ x = x_from_ratio(y)
33
+ if !x
34
+ len = LENGTHS[get_bits(2)]
35
+ x = get_bits(len) + 1
36
+ end
37
+ @width, @height = x, y
38
+ end
39
+
40
+ def get_bits(size)
41
+ if @words.size < (@bit_counter + size) / 16 + 1
42
+ @words += @stream.read(4).unpack('vv')
43
+ end
44
+
45
+ dest_pos = 0
46
+ dest = 0
47
+ size.times do
48
+ word = @bit_counter / 16
49
+ source_pos = @bit_counter % 16
50
+ dest |= ((@words[word] & (1 << source_pos)) > 0 ? 1 : 0) << dest_pos
51
+ dest_pos += 1
52
+ @bit_counter += 1
53
+ end
54
+ dest
55
+ end
56
+
57
+ def x_from_ratio(y)
58
+ ratio = get_bits(3)
59
+ if ratio == 0
60
+ return nil
61
+ else
62
+ return (y * MULTIPLIERS[ratio - 1]).to_i
63
+ end
64
+ end
65
+ end
66
+
67
+ def parse_size_for_jxl
68
+ if @stream.peek(2) == "\xFF\x0A".b
69
+ JXL.new(@stream).read_size_header
70
+ else
71
+ bmff = IsoBmff.new(@stream)
72
+ bmff.width_and_height
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,26 @@
1
+ module FastImageParsing
2
+ class Png < ImageBase # :nodoc:
3
+ def dimensions
4
+ @stream.read(25)[16..24].unpack('NN')
5
+ end
6
+
7
+ def animated?
8
+ # Signature (8) + IHDR chunk (4 + 4 + 13 + 4)
9
+ @stream.read(33)
10
+
11
+ loop do
12
+ length = @stream.read(4).unpack("L>")[0]
13
+ type = @stream.read(4)
14
+
15
+ case type
16
+ when "acTL"
17
+ return true
18
+ when "IDAT"
19
+ return false
20
+ end
21
+
22
+ @stream.skip(length + 4)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,7 @@
1
+ module FastImageParsing
2
+ class Psd < ImageBase # :nodoc:
3
+ def dimensions
4
+ @stream.read(26).unpack("x14NN").reverse
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ module FastImageParsing
2
+ module StreamUtil # :nodoc:
3
+ def read_byte
4
+ read(1)[0].ord
5
+ end
6
+
7
+ def read_int
8
+ read(2).unpack('n')[0]
9
+ end
10
+
11
+ def read_string_int
12
+ value = []
13
+ while read(1) =~ /(\d)/
14
+ value << $1
15
+ end
16
+ value.join.to_i
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,69 @@
1
+ module FastImageParsing
2
+ class Svg < ImageBase # :nodoc:
3
+ def dimensions
4
+ @width, @height, @ratio, @viewbox_width, @viewbox_height = nil
5
+
6
+ parse_svg
7
+
8
+ if @width && @height
9
+ [@width, @height]
10
+ elsif @width && @ratio
11
+ [@width, @width / @ratio]
12
+ elsif @height && @ratio
13
+ [@height * @ratio, @height]
14
+ elsif @viewbox_width && @viewbox_height
15
+ [@viewbox_width, @viewbox_height]
16
+ else
17
+ nil
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def parse_svg
24
+ attr_name = []
25
+ state = nil
26
+
27
+ while (char = @stream.read(1)) && state != :stop do
28
+ case char
29
+ when "="
30
+ if attr_name.join =~ /width/i
31
+ @stream.read(1)
32
+ @width = @stream.read_string_int
33
+ return if @height
34
+ elsif attr_name.join =~ /height/i
35
+ @stream.read(1)
36
+ @height = @stream.read_string_int
37
+ return if @width
38
+ elsif attr_name.join =~ /viewbox/i
39
+ values = attr_value.split(/\s/)
40
+ if values[2].to_f > 0 && values[3].to_f > 0
41
+ @ratio = values[2].to_f / values[3].to_f
42
+ @viewbox_width = values[2].to_i
43
+ @viewbox_height = values[3].to_i
44
+ end
45
+ end
46
+ when /\w/
47
+ attr_name << char
48
+ when "<"
49
+ attr_name = [char]
50
+ when ">"
51
+ state = :stop if state == :started
52
+ else
53
+ state = :started if attr_name.join == "<svg"
54
+ attr_name.clear
55
+ end
56
+ end
57
+ end
58
+
59
+ def attr_value
60
+ @stream.read(1)
61
+
62
+ value = []
63
+ while @stream.read(1) =~ /([^"])/
64
+ value << $1
65
+ end
66
+ value.join
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,16 @@
1
+ module FastImageParsing
2
+ class Tiff < ImageBase # :nodoc:
3
+ def initialize(stream)
4
+ @stream = stream
5
+ end
6
+
7
+ def dimensions
8
+ exif = Exif.new(@stream)
9
+ if exif.rotated?
10
+ [exif.height, exif.width, exif.orientation]
11
+ else
12
+ [exif.width, exif.height, exif.orientation]
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,69 @@
1
+ module FastImageParsing
2
+ class TypeParser
3
+ def initialize(stream)
4
+ @stream = stream
5
+ end
6
+
7
+ # type will use peek to get enough bytes to determing the type of the image
8
+ def type
9
+ parsed_type = case @stream.peek(2)
10
+ when "BM"
11
+ :bmp
12
+ when "GI"
13
+ :gif
14
+ when 0xff.chr + 0xd8.chr
15
+ :jpeg
16
+ when 0x89.chr + "P"
17
+ :png
18
+ when "II", "MM"
19
+ case @stream.peek(11)[8..10]
20
+ when "APC", "CR\002"
21
+ nil # do not recognise CRW or CR2 as tiff
22
+ else
23
+ :tiff
24
+ end
25
+ when '8B'
26
+ :psd
27
+ when "\xFF\x0A".b
28
+ :jxl
29
+ when "\0\0"
30
+ case @stream.peek(3).bytes.to_a.last
31
+ when 0
32
+ # http://www.ftyps.com/what.html
33
+ case @stream.peek(12)[4..-1]
34
+ when "ftypavif"
35
+ :avif
36
+ when "ftypavis"
37
+ :avif
38
+ when "ftypheic"
39
+ :heic
40
+ when "ftypmif1"
41
+ :heif
42
+ else
43
+ if @stream.peek(7)[4..-1] == 'JXL'
44
+ :jxl
45
+ end
46
+ end
47
+ # ico has either a 1 (for ico format) or 2 (for cursor) at offset 3
48
+ when 1 then :ico
49
+ when 2 then :cur
50
+ end
51
+ when "RI"
52
+ :webp if @stream.peek(12)[8..11] == "WEBP"
53
+ when "<s"
54
+ :svg if @stream.peek(4) == "<svg"
55
+ when /\s\s|\s<|<[?!]/, 0xef.chr + 0xbb.chr
56
+ # Peek 10 more chars each time, and if end of file is reached just raise
57
+ # unknown. We assume the <svg tag cannot be within 10 chars of the end of
58
+ # the file, and is within the first 1000 chars.
59
+ begin
60
+ :svg if (1..100).detect {|n| @stream.peek(10 * n).include?("<svg")}
61
+ rescue FiberError, FastImage::CannotParseImage
62
+ nil
63
+ end
64
+ end
65
+
66
+ parsed_type or raise FastImage::UnknownImageType
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,60 @@
1
+ module FastImageParsing
2
+ class Webp < ImageBase # :nodoc:
3
+ def dimensions
4
+ vp8 = @stream.read(16)[12..15]
5
+ _len = @stream.read(4).unpack("V")
6
+ case vp8
7
+ when "VP8 "
8
+ parse_size_vp8
9
+ when "VP8L"
10
+ parse_size_vp8l
11
+ when "VP8X"
12
+ parse_size_vp8x
13
+ else
14
+ nil
15
+ end
16
+ end
17
+
18
+ def animated?
19
+ vp8 = @stream.read(16)[12..15]
20
+ _len = @stream.read(4).unpack("V")
21
+ case vp8
22
+ when "VP8 "
23
+ false
24
+ when "VP8L"
25
+ false
26
+ when "VP8X"
27
+ flags = @stream.read(4).unpack("C")[0]
28
+ flags & 2 > 0
29
+ else
30
+ nil
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def parse_size_vp8
37
+ w, h = @stream.read(10).unpack("@6vv")
38
+ [w & 0x3fff, h & 0x3fff]
39
+ end
40
+
41
+ def parse_size_vp8l
42
+ @stream.skip(1) # 0x2f
43
+ b1, b2, b3, b4 = @stream.read(4).bytes.to_a
44
+ [1 + (((b2 & 0x3f) << 8) | b1), 1 + (((b4 & 0xF) << 10) | (b3 << 2) | ((b2 & 0xC0) >> 6))]
45
+ end
46
+
47
+ def parse_size_vp8x
48
+ flags = @stream.read(4).unpack("C")[0]
49
+ b1, b2, b3, b4, b5, b6 = @stream.read(6).unpack("CCCCCC")
50
+ width, height = 1 + b1 + (b2 << 8) + (b3 << 16), 1 + b4 + (b5 << 8) + (b6 << 16)
51
+
52
+ if flags & 8 > 0 # exif
53
+ # parse exif for orientation
54
+ # TODO: find or create test images for this
55
+ end
56
+
57
+ [width, height]
58
+ end
59
+ end
60
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class FastImage
4
- VERSION = '2.3.1'
4
+ VERSION = '2.4.0'
5
5
  end