Merge pull request #125 from stefanoverna/heic-format

Add support for HEIC format
This commit is contained in:
Stephen Sykes 2021-04-05 14:59:30 +01:00 committed by GitHub
commit 31ea417d8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 181 additions and 13 deletions

View File

@ -89,6 +89,8 @@ class FastImage
LocalFileChunkSize = 256 unless const_defined?(:LocalFileChunkSize) LocalFileChunkSize = 256 unless const_defined?(:LocalFileChunkSize)
SUPPORTED_IMAGE_TYPES = [:bmp, :gif, :jpeg, :png, :tiff, :psd, :heic, :heif, :webp, :svg, :ico, :cur].freeze
# Returns an array containing the width and height of the image. # Returns an array containing the width and height of the image.
# It will return nil if the image could not be fetched, or if the image type was not recognised. # It will return nil if the image could not be fetched, or if the image type was not recognised.
# #
@ -538,8 +540,21 @@ class FastImage
when '8B' when '8B'
:psd :psd
when "\0\0" when "\0\0"
# ico has either a 1 (for ico format) or 2 (for cursor) at offset 3
case @stream.peek(3).bytes.to_a.last case @stream.peek(3).bytes.to_a.last
when 0
# http://www.ftyps.com/what.html
# HEIC is composed of nested "boxes". Each box has a header composed of
# - Size (32 bit integer)
# - Box type (4 chars)
# - Extended size: only if size === 1, the type field is followed by 64 bit integer of extended size
# - Payload: Type-dependent
case @stream.peek(12)[4..-1]
when "ftypheic"
:heic
when "ftypmif1"
:heif
end
# ico has either a 1 (for ico format) or 2 (for cursor) at offset 3
when 1 then :ico when 1 then :ico
when 2 then :cur when 2 then :cur
end end
@ -568,6 +583,152 @@ class FastImage
end end
alias_method :parse_size_for_cur, :parse_size_for_ico alias_method :parse_size_for_cur, :parse_size_for_ico
class Heic # :nodoc:
def initialize(stream)
@stream = stream
end
def width_and_height
@max_size = nil
@primary_box = nil
@ipma_boxes = []
@ispe_boxes = []
@final_size = nil
catch :finish do
read_boxes!
end
@final_size
end
private
def read_boxes!(max_read_bytes = nil)
end_pos = max_read_bytes.nil? ? nil : @stream.pos + max_read_bytes
index = 0
loop do
return if end_pos && @stream.pos >= end_pos
box_type, box_size = read_box_header!
case box_type
when "meta"
handle_meta_box(box_size)
when "pitm"
handle_pitm_box(box_size)
when "ipma"
handle_ipma_box(box_size)
when "hdlr"
handle_hdlr_box(box_size)
when "iprp", "ipco"
read_boxes!(box_size)
when "ispe"
handle_ispe_box(box_size, index)
when "mdat"
throw :finish
else
@stream.read(box_size)
end
index += 1
end
end
def handle_ispe_box(box_size, index)
throw :finish if box_size < 12
data = @stream.read(box_size)
width, height = data[4...12].unpack("N2")
@ispe_boxes << { index: index, size: [width, height] }
end
def handle_hdlr_box(box_size)
throw :finish if box_size < 12
data = @stream.read(box_size)
throw :finish if data[8...12] != "pict"
end
def handle_ipma_box(box_size)
@stream.read(3)
flags3 = read_uint8!
entries_count = read_uint32!
entries_count.times do
id = read_uint16!
essen_count = read_uint8!
essen_count.times do
property_index = read_uint8! & 0x7F
if flags3 & 1 == 1
property_index = (property_index << 7) + read_uint8!
end
@ipma_boxes << { id: id, property_index: property_index - 1 }
end
end
end
def handle_pitm_box(box_size)
data = @stream.read(box_size)
@primary_box = data[4...6].unpack("S>")[0]
end
def handle_meta_box(box_size)
throw :finish if box_size < 4
@stream.read(4)
read_boxes!(box_size - 4)
throw :finish if !@primary_box
primary_indices = @ipma_boxes
.select { |box| box[:id] == @primary_box }
.map { |box| box[:property_index] }
ispe_box = @ispe_boxes.find do |box|
primary_indices.include?(box[:index])
end
if ispe_box
@final_size = ispe_box[:size]
end
throw :finish
end
def read_box_header!
size = read_uint32!
type = @stream.read(4)
[type, size - 8]
end
def read_uint8!
@stream.read(1).unpack("C")[0]
end
def read_uint16!
@stream.read(2).unpack("S>")[0]
end
def read_uint32!
@stream.read(4).unpack("N")[0]
end
end
def parse_size_for_heic
heic = Heic.new(@stream)
heic.width_and_height
end
def parse_size_for_heif
heic = Heic.new(@stream)
heic.width_and_height
end
class Gif # :nodoc: class Gif # :nodoc:
def initialize(stream) def initialize(stream)
@stream = stream @stream = stream

BIN
test/fixtures/heic/heic-collection.heic vendored Normal file

Binary file not shown.

BIN
test/fixtures/heic/heic-empty.heic vendored Normal file

Binary file not shown.

BIN
test/fixtures/heic/heic-iphone.heic vendored Normal file

Binary file not shown.

BIN
test/fixtures/heic/heic-iphone7.heic vendored Normal file

Binary file not shown.

BIN
test/fixtures/heic/heic-maybebroken.HEIC vendored Normal file

Binary file not shown.

BIN
test/fixtures/heic/heic-single.heic vendored Normal file

Binary file not shown.

BIN
test/fixtures/heic/test.heic vendored Normal file

Binary file not shown.

View File

@ -40,7 +40,14 @@ GoodFixtures = {
"test3.svg" => [:svg, [255, 48]], "test3.svg" => [:svg, [255, 48]],
"test4.svg" => [:svg, [271, 271]], "test4.svg" => [:svg, [271, 271]],
"test5.svg" => [:svg, [255, 48]], "test5.svg" => [:svg, [255, 48]],
"orient_6.jpg"=>[:jpeg, [1250,2500]] "orient_6.jpg"=>[:jpeg, [1250,2500]],
"heic/test.heic"=>[:heic, [700,476]],
"heic/heic-empty.heic"=>[:heic, [3992,2992]],
"heic/heic-iphone.heic"=>[:heic,[4032,3024]],
"heic/heic-iphone7.heic"=>[:heic,[4032,3024]],
"heic/heic-maybebroken.HEIC"=>[:heic,[4032,3024]],
"heic/heic-single.heic"=>[:heif,[1440,960]],
"heic/heic-collection.heic"=>[:heif,[1440,960]],
} }
BadFixtures = [ BadFixtures = [
@ -49,7 +56,7 @@ BadFixtures = [
"test.xml", "test.xml",
"test2.xml", "test2.xml",
"a.CR2", "a.CR2",
"a.CRW" "a.CRW",
] ]
# man.ico courtesy of http://www.iconseeker.com/search-icon/artists-valley-sample/business-man-blue.html # man.ico courtesy of http://www.iconseeker.com/search-icon/artists-valley-sample/business-man-blue.html
# test_rgb.ct courtesy of http://fileformats.archiveteam.org/wiki/Scitex_CT # test_rgb.ct courtesy of http://fileformats.archiveteam.org/wiki/Scitex_CT
@ -94,15 +101,15 @@ end
class FastImageTest < Test::Unit::TestCase class FastImageTest < Test::Unit::TestCase
def test_should_report_type_correctly def test_should_report_type_correctly
GoodFixtures.each do |fn, info| GoodFixtures.each do |fn, info|
assert_equal info[0], FastImage.type(TestUrl + fn) assert_equal info[0], FastImage.type(TestUrl + fn), "type of image #{fn} must be #{info[0]}"
assert_equal info[0], FastImage.type(TestUrl + fn, :raise_on_failure=>true) assert_equal info[0], FastImage.type(TestUrl + fn, :raise_on_failure=>true), "type of image #{fn} must be #{info[0]}"
end end
end end
def test_should_report_size_correctly def test_should_report_size_correctly
GoodFixtures.each do |fn, info| GoodFixtures.each do |fn, info|
assert_equal info[1], FastImage.size(TestUrl + fn) assert_equal info[1], FastImage.size(TestUrl + fn), "size for #{fn} must be #{info[1]}"
assert_equal info[1], FastImage.size(TestUrl + fn, :raise_on_failure=>true) assert_equal info[1], FastImage.size(TestUrl + fn, :raise_on_failure=>true), "size for #{fn} must be #{info[1]}"
end end
end end
@ -171,13 +178,13 @@ class FastImageTest < Test::Unit::TestCase
def test_should_report_type_correctly_for_local_files def test_should_report_type_correctly_for_local_files
GoodFixtures.each do |fn, info| GoodFixtures.each do |fn, info|
assert_equal info[0], FastImage.type(File.join(FixturePath, fn)) assert_equal info[0], FastImage.type(File.join(FixturePath, fn)), "type of image #{fn} must be #{info[0]}"
end end
end end
def test_should_report_size_correctly_for_local_files def test_should_report_size_correctly_for_local_files
GoodFixtures.each do |fn, info| GoodFixtures.each do |fn, info|
assert_equal info[1], FastImage.size(File.join(FixturePath, fn)) assert_equal info[1], FastImage.size(File.join(FixturePath, fn)), "size for #{fn} must be #{info[1]}"
end end
end end
@ -188,7 +195,7 @@ class FastImageTest < Test::Unit::TestCase
def test_should_report_type_correctly_for_ios def test_should_report_type_correctly_for_ios
GoodFixtures.each do |fn, info| GoodFixtures.each do |fn, info|
File.open(File.join(FixturePath, fn), "r") do |io| File.open(File.join(FixturePath, fn), "r") do |io|
assert_equal info[0], FastImage.type(io) assert_equal info[0], FastImage.type(io), "type of image #{fn} must be #{info[0]}"
end end
end end
end end
@ -196,7 +203,7 @@ class FastImageTest < Test::Unit::TestCase
def test_should_report_size_correctly_for_ios def test_should_report_size_correctly_for_ios
GoodFixtures.each do |fn, info| GoodFixtures.each do |fn, info|
File.open(File.join(FixturePath, fn), "r") do |io| File.open(File.join(FixturePath, fn), "r") do |io|
assert_equal info[1], FastImage.size(io) assert_equal info[1], FastImage.size(io), "size for #{fn} must be #{info[1]}"
end end
end end
end end
@ -205,7 +212,7 @@ class FastImageTest < Test::Unit::TestCase
GoodFixtures.each do |fn, info| GoodFixtures.each do |fn, info|
File.open(File.join(FixturePath, fn), "r") do |io| File.open(File.join(FixturePath, fn), "r") do |io|
io.read io.read
assert_equal info[0], FastImage.type(io) assert_equal info[0], FastImage.type(io), "type of image #{fn} must be #{info[0]}"
end end
end end
end end
@ -214,7 +221,7 @@ class FastImageTest < Test::Unit::TestCase
GoodFixtures.each do |fn, info| GoodFixtures.each do |fn, info|
File.open(File.join(FixturePath, fn), "r") do |io| File.open(File.join(FixturePath, fn), "r") do |io|
io.read io.read
assert_equal info[1], FastImage.size(io) assert_equal info[1], FastImage.size(io), "size for #{fn} must be #{info[1]}"
end end
end end
end end