diff --git a/lib/fastimage.rb b/lib/fastimage.rb index d6d8671..eac44ab 100644 --- a/lib/fastimage.rb +++ b/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 diff --git a/test/fixtures/heic/heic-collection.heic b/test/fixtures/heic/heic-collection.heic new file mode 100644 index 0000000..72ccf5b Binary files /dev/null and b/test/fixtures/heic/heic-collection.heic differ diff --git a/test/fixtures/heic/heic-empty.heic b/test/fixtures/heic/heic-empty.heic new file mode 100644 index 0000000..7ba2820 Binary files /dev/null and b/test/fixtures/heic/heic-empty.heic differ diff --git a/test/fixtures/heic/heic-iphone.heic b/test/fixtures/heic/heic-iphone.heic new file mode 100644 index 0000000..9f10121 Binary files /dev/null and b/test/fixtures/heic/heic-iphone.heic differ diff --git a/test/fixtures/heic/heic-iphone7.heic b/test/fixtures/heic/heic-iphone7.heic new file mode 100644 index 0000000..1055d35 Binary files /dev/null and b/test/fixtures/heic/heic-iphone7.heic differ diff --git a/test/fixtures/heic/heic-maybebroken.HEIC b/test/fixtures/heic/heic-maybebroken.HEIC new file mode 100644 index 0000000..9c63182 Binary files /dev/null and b/test/fixtures/heic/heic-maybebroken.HEIC differ diff --git a/test/fixtures/heic/heic-single.heic b/test/fixtures/heic/heic-single.heic new file mode 100644 index 0000000..00cc549 Binary files /dev/null and b/test/fixtures/heic/heic-single.heic differ diff --git a/test/fixtures/heic/test.heic b/test/fixtures/heic/test.heic new file mode 100644 index 0000000..efd119a Binary files /dev/null and b/test/fixtures/heic/test.heic differ diff --git a/test/test.rb b/test/test.rb index f0c68a0..6b9833c 100644 --- a/test/test.rb +++ b/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