HEIC/HEIF format detection

This commit is contained in:
Stefano Verna 2021-03-02 15:07:12 +01:00
parent 9a1400ebcc
commit 58dcc3072f
9 changed files with 180 additions and 13 deletions

View File

@ -538,8 +538,21 @@ class FastImage
when '8B'
:psd
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
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 2 then :cur
end
@ -568,6 +581,153 @@ class FastImage
end
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
end_pos.nil? || @stream.pos < end_pos or
return
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)
box_size >= 12 or throw :finish
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)
box_size >= 12 or throw :finish
data = @stream.read(box_size)
data[8...12] == "pict" or throw :finish
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].unpack1("S>")
end
def handle_meta_box(box_size)
box_size >= 4 or throw :finish
@stream.read(4)
read_boxes!(box_size - 4)
@primary_box or throw :finish
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).unpack1("C")
end
def read_uint16!
@stream.read(2).unpack1("S>")
end
def read_uint32!
@stream.read(4).unpack1("N")
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:
def initialize(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]],
"test4.svg" => [:svg, [271, 271]],
"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 = [
@ -49,7 +56,7 @@ BadFixtures = [
"test.xml",
"test2.xml",
"a.CR2",
"a.CRW"
"a.CRW",
]
# 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
@ -94,15 +101,15 @@ end
class FastImageTest < Test::Unit::TestCase
def test_should_report_type_correctly
GoodFixtures.each do |fn, info|
assert_equal info[0], FastImage.type(TestUrl + fn)
assert_equal info[0], FastImage.type(TestUrl + fn, :raise_on_failure=>true)
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), "type of image #{fn} must be #{info[0]}"
end
end
def test_should_report_size_correctly
GoodFixtures.each do |fn, info|
assert_equal info[1], FastImage.size(TestUrl + fn)
assert_equal info[1], FastImage.size(TestUrl + fn, :raise_on_failure=>true)
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), "size for #{fn} must be #{info[1]}"
end
end
@ -171,13 +178,13 @@ class FastImageTest < Test::Unit::TestCase
def test_should_report_type_correctly_for_local_files
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
def test_should_report_size_correctly_for_local_files
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
@ -188,7 +195,7 @@ class FastImageTest < Test::Unit::TestCase
def test_should_report_type_correctly_for_ios
GoodFixtures.each do |fn, info|
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
@ -196,7 +203,7 @@ class FastImageTest < Test::Unit::TestCase
def test_should_report_size_correctly_for_ios
GoodFixtures.each do |fn, info|
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
@ -205,7 +212,7 @@ class FastImageTest < Test::Unit::TestCase
GoodFixtures.each do |fn, info|
File.open(File.join(FixturePath, fn), "r") do |io|
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
@ -214,7 +221,7 @@ class FastImageTest < Test::Unit::TestCase
GoodFixtures.each do |fn, info|
File.open(File.join(FixturePath, fn), "r") do |io|
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