fastimage 2.3.0 → 2.4.0
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 +4 -4
- data/README.md +64 -20
- data/lib/fastimage/fastimage.rb +471 -0
- data/lib/fastimage/fastimage_parsing/avif.rb +12 -0
- data/lib/fastimage/fastimage_parsing/bmp.rb +17 -0
- data/lib/fastimage/fastimage_parsing/exif.rb +76 -0
- data/lib/fastimage/fastimage_parsing/fiber_stream.rb +58 -0
- data/lib/fastimage/fastimage_parsing/gif.rb +63 -0
- data/lib/fastimage/fastimage_parsing/heic.rb +8 -0
- data/lib/fastimage/fastimage_parsing/ico.rb +9 -0
- data/lib/fastimage/fastimage_parsing/image_base.rb +17 -0
- data/lib/fastimage/fastimage_parsing/iso_bmff.rb +176 -0
- data/lib/fastimage/fastimage_parsing/jpeg.rb +52 -0
- data/lib/fastimage/fastimage_parsing/jxl.rb +13 -0
- data/lib/fastimage/fastimage_parsing/jxlc.rb +75 -0
- data/lib/fastimage/fastimage_parsing/png.rb +26 -0
- data/lib/fastimage/fastimage_parsing/psd.rb +7 -0
- data/lib/fastimage/fastimage_parsing/stream_util.rb +19 -0
- data/lib/fastimage/fastimage_parsing/svg.rb +69 -0
- data/lib/fastimage/fastimage_parsing/tiff.rb +16 -0
- data/lib/fastimage/fastimage_parsing/type_parser.rb +69 -0
- data/lib/fastimage/fastimage_parsing/webp.rb +60 -0
- data/lib/fastimage/version.rb +1 -1
- data/lib/fastimage.rb +3 -1072
- metadata +23 -6
@@ -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,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,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
|