fastimage 2.3.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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