Skip to content

Commit

Permalink
Split and refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
sdsykes committed Apr 1, 2024
1 parent 054f563 commit b50c834
Show file tree
Hide file tree
Showing 21 changed files with 1,271 additions and 1,159 deletions.
1,162 changes: 3 additions & 1,159 deletions lib/fastimage.rb

Large diffs are not rendered by default.

435 changes: 435 additions & 0 deletions lib/fastimage/fastimage.rb

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions lib/fastimage/fastimage_parsing/avif.rb
@@ -0,0 +1,12 @@
module FastImageParsing
class Avif < ImageBase # :nodoc:
def dimensions
bmff = IsoBmff.new(@stream)
[bmff.width, bmff.height]
end

def animated?
@stream.peek(12)[4..-1] == "ftypavis"
end
end
end
17 changes: 17 additions & 0 deletions lib/fastimage/fastimage_parsing/bmp.rb
@@ -0,0 +1,17 @@
module FastImageParsing
class Bmp < ImageBase # :nodoc:
def dimensions
d = @stream.read(32)[14..28]
header = d.unpack("C")[0]

result = if header == 12
d[4..8].unpack('SS')
else
d[4..-1].unpack('l<l<')
end

# ImageHeight is expressed in pixels. The absolute value is necessary because ImageHeight can be negative
[result.first, result.last.abs]
end
end
end
69 changes: 69 additions & 0 deletions lib/fastimage/fastimage_parsing/exif.rb
@@ -0,0 +1,69 @@
module FastImageParsing
class Exif # :nodoc:
attr_reader :width, :height, :orientation

def initialize(stream)
@stream = stream
@width, @height, @orientation = nil
parse_exif
end

def rotated?
@orientation >= 5
end

private

def get_exif_byte_order
byte_order = @stream.read(2)
case byte_order
when 'II'
@short, @long = 'v', 'V'
when 'MM'
@short, @long = 'n', 'N'
else
raise FastImage::CannotParseImage
end
end

def parse_exif_ifd
tag_count = @stream.read(2).unpack(@short)[0]
tag_count.downto(1) do
type = @stream.read(2).unpack(@short)[0]
@stream.read(6)
data = @stream.read(2).unpack(@short)[0]
case type
when 0x0100 # image width
@width = data
when 0x0101 # image height
@height = data
when 0x0112 # orientation
@orientation = data
end
if @width && @height && @orientation
return # no need to parse more
end
@stream.read(2)
end
end

def parse_exif
@start_byte = @stream.pos

get_exif_byte_order

@stream.read(2) # 42

offset = @stream.read(4).unpack(@long)[0]
if @stream.respond_to?(:skip)
@stream.skip(offset - 8)
else
@stream.read(offset - 8)
end

parse_exif_ifd

@orientation ||= 1
end
end
end
58 changes: 58 additions & 0 deletions lib/fastimage/fastimage_parsing/fiber_stream.rb
@@ -0,0 +1,58 @@
module FastImageParsing
class FiberStream # :nodoc:
include StreamUtil
attr_reader :pos

# read_fiber should return nil if it no longer has anything to return when resumed
# so the result of the whole Fiber block should be set to be nil in case yield is no
# longer called
def initialize(read_fiber)
@read_fiber = read_fiber
@pos = 0
@strpos = 0
@str = ''
end

# Peeking beyond the end of the input will raise
def peek(n)
while @strpos + n > @str.size
unused_str = @str[@strpos..-1]

new_string = @read_fiber.resume
raise FastImage::CannotParseImage if !new_string
# we are dealing with bytes here, so force the encoding
new_string.force_encoding("ASCII-8BIT") if new_string.respond_to? :force_encoding

@str = unused_str + new_string
@strpos = 0
end

@str[@strpos, n]
end

def read(n)
result = peek(n)
@strpos += n
@pos += n
result
end

def skip(n)
discarded = 0
fetched = @str[@strpos..-1].size
while n > fetched
discarded += @str[@strpos..-1].size
new_string = @read_fiber.resume
raise FastImage::CannotParseImage if !new_string

new_string.force_encoding("ASCII-8BIT") if new_string.respond_to? :force_encoding

fetched += new_string.size
@str = new_string
@strpos = 0
end
@strpos = @strpos + n - discarded
@pos += n
end
end
end
63 changes: 63 additions & 0 deletions lib/fastimage/fastimage_parsing/gif.rb
@@ -0,0 +1,63 @@
module FastImageParsing
class Gif < ImageBase # :nodoc:
def dimensions
@stream.read(11)[6..10].unpack('SS')
end

# Checks for multiple frames
def animated?
frames = 0

# "GIF" + version (3) + width (2) + height (2)
@stream.skip(10)

# fields (1) + bg color (1) + pixel ratio (1)
fields = @stream.read(3).unpack("CCC")[0]
if fields & 0x80 != 0 # Global Color Table
# 2 * (depth + 1) colors, each occupying 3 bytes (RGB)
@stream.skip(3 * 2 ** ((fields & 0x7) + 1))
end

loop do
block_type = @stream.read(1).unpack("C")[0]

if block_type == 0x21 # Graphic Control Extension
# extension type (1) + size (1)
size = @stream.read(2).unpack("CC")[1]
@stream.skip(size)
skip_sub_blocks
elsif block_type == 0x2C # Image Descriptor
frames += 1
return true if frames > 1

# left position (2) + top position (2) + width (2) + height (2) + fields (1)
fields = @stream.read(9).unpack("SSSSC")[4]
if fields & 0x80 != 0 # Local Color Table
# 2 * (depth + 1) colors, each occupying 3 bytes (RGB)
@stream.skip(3 * 2 ** ((fields & 0x7) + 1))
end

@stream.skip(1) # LZW min code size (1)
skip_sub_blocks
else
break # unrecognized block
end
end

false
end

private

def skip_sub_blocks
loop do
size = @stream.read(1).unpack("C")[0]
if size == 0
break
else
@stream.skip(size)
end
end
end
end
end
8 changes: 8 additions & 0 deletions lib/fastimage/fastimage_parsing/heic.rb
@@ -0,0 +1,8 @@
module FastImageParsing
class Heic < ImageBase # :nodoc:
def dimensions
bmff = IsoBmff.new(@stream)
[bmff.width, bmff.height]
end
end
end
9 changes: 9 additions & 0 deletions lib/fastimage/fastimage_parsing/ico.rb
@@ -0,0 +1,9 @@
module FastImageParsing
class Ico < ImageBase
def dimensions
icons = @stream.read(6)[4..5].unpack('v').first
sizes = icons.times.map { @stream.read(16).unpack('C2').map { |x| x == 0 ? 256 : x } }.sort_by { |w,h| w * h }
sizes.last
end
end
end
17 changes: 17 additions & 0 deletions lib/fastimage/fastimage_parsing/image_base.rb
@@ -0,0 +1,17 @@
module FastImageParsing
class ImageBase # :nodoc:
def initialize(stream)
@stream = stream
end

# Implement in subclasses
def dimensions
raise NotImplementedError
end

# Implement in subclasses if appropriate
def animated?
nil
end
end
end

0 comments on commit b50c834

Please sign in to comment.