mirror of
https://github.com/sdsykes/fastimage.git
synced 2025-09-20 00:01:59 -04:00
HEIC/HEIF format detection
This commit is contained in:
parent
9a1400ebcc
commit
58dcc3072f
162
lib/fastimage.rb
162
lib/fastimage.rb
@ -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
BIN
test/fixtures/heic/heic-collection.heic
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/heic/heic-empty.heic
vendored
Normal file
BIN
test/fixtures/heic/heic-empty.heic
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/heic/heic-iphone.heic
vendored
Normal file
BIN
test/fixtures/heic/heic-iphone.heic
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/heic/heic-iphone7.heic
vendored
Normal file
BIN
test/fixtures/heic/heic-iphone7.heic
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/heic/heic-maybebroken.HEIC
vendored
Normal file
BIN
test/fixtures/heic/heic-maybebroken.HEIC
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/heic/heic-single.heic
vendored
Normal file
BIN
test/fixtures/heic/heic-single.heic
vendored
Normal file
Binary file not shown.
BIN
test/fixtures/heic/test.heic
vendored
Normal file
BIN
test/fixtures/heic/test.heic
vendored
Normal file
Binary file not shown.
31
test/test.rb
31
test/test.rb
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user