diff --git a/src/ImageSharp/Formats/Heic/HeicDecoderCore.cs b/src/ImageSharp/Formats/Heic/HeicDecoderCore.cs index 93efcd21d1..376dd466ed 100644 --- a/src/ImageSharp/Formats/Heic/HeicDecoderCore.cs +++ b/src/ImageSharp/Formats/Heic/HeicDecoderCore.cs @@ -27,9 +27,11 @@ internal sealed class HeicDecoderCore : IImageDecoderInternals private uint primaryItem; - private List items; + private readonly List items; - private List itemLinks; + private readonly List itemLinks; + + private readonly byte[] buffer; /// /// Initializes a new instance of the class. @@ -42,6 +44,7 @@ internal sealed class HeicDecoderCore : IImageDecoderInternals this.metadata = new ImageMetadata(); this.items = new List(); this.itemLinks = new List(); + this.buffer = new byte[80]; } /// @@ -83,6 +86,7 @@ internal sealed class HeicDecoderCore : IImageDecoderInternals var image = new Image(this.configuration, item.Extent.Width, item.Extent.Height, this.metadata); + // TODO: Parse pixel data Buffer2D pixels = image.GetRootFramePixelBuffer(); return image; @@ -103,6 +107,7 @@ internal sealed class HeicDecoderCore : IImageDecoderInternals break; default: // Silently skip all other box types. + SkipBox(stream, length); break; } } @@ -118,10 +123,9 @@ internal sealed class HeicDecoderCore : IImageDecoderInternals private bool CheckFileTypeBox(BufferedReadStream stream) { - var boxLength = this.ReadBoxHeader(stream, out Heic4CharCode boxType); - Span buffer = stackalloc byte[(int)boxLength]; - stream.Read(buffer); - var majorBrand = BinaryPrimitives.ReadUInt32BigEndian(buffer); + long boxLength = this.ReadBoxHeader(stream, out Heic4CharCode boxType); + Span buffer = this.ReadIntoBuffer(stream, boxLength); + uint majorBrand = BinaryPrimitives.ReadUInt32BigEndian(buffer); // TODO: Interpret minorVersion and compatible brands. return boxType == Heic4CharCode.ftyp && majorBrand == (uint)Heic4CharCode.heic; @@ -130,8 +134,7 @@ internal sealed class HeicDecoderCore : IImageDecoderInternals private long ReadBoxHeader(BufferedReadStream stream, out Heic4CharCode boxType) { // Read 4 bytes of length of box - Span buf = stackalloc byte[8]; - stream.Read(buf); + Span buf = this.ReadIntoBuffer(stream, 8); long boxSize = BinaryPrimitives.ReadUInt32BigEndian(buf); long headerSize = 8; @@ -140,7 +143,7 @@ internal sealed class HeicDecoderCore : IImageDecoderInternals if (boxSize == 1) { - stream.Read(buf); + buf = this.ReadIntoBuffer(stream, 8); boxSize = (long)BinaryPrimitives.ReadUInt64BigEndian(buf); headerSize += 8; } @@ -185,12 +188,21 @@ internal sealed class HeicDecoderCore : IImageDecoderInternals this.ParsePrimaryItem(stream, length); break; case Heic4CharCode.dinf: - case Heic4CharCode.grpl: + SkipBox(stream, length); + break; case Heic4CharCode.hdlr: + SkipBox(stream, length); + break; case Heic4CharCode.idat: + SkipBox(stream, length); + break; case Heic4CharCode.iloc: + this.ParseItemLocation(stream, length); + break; + case Heic4CharCode.grpl: case Heic4CharCode.ipro: // TODO: Implement + SkipBox(stream, length); break; default: throw new ImageFormatException($"Unknown metadata box type of '{Enum.GetName(boxType)}'"); @@ -200,22 +212,12 @@ internal sealed class HeicDecoderCore : IImageDecoderInternals private void ParseItemInfo(BufferedReadStream stream, long boxLength) { - Span buffer = stackalloc byte[(int)boxLength]; - stream.Read(buffer); + Span buffer = this.ReadIntoBuffer(stream, boxLength); uint entryCount; int bytesRead = 0; - if (buffer[bytesRead] == 0) - { - bytesRead += 4; - entryCount = BinaryPrimitives.ReadUInt16BigEndian(buffer[bytesRead..]); - bytesRead += 2; - } - else - { - bytesRead += 4; - entryCount = BinaryPrimitives.ReadUInt32BigEndian(buffer[bytesRead..]); - bytesRead += 4; - } + byte version = buffer[bytesRead]; + bytesRead += 4; + entryCount = ReadUInt16Or32(buffer, version != 0, ref bytesRead); for (uint i = 0; i < entryCount; i++) { @@ -267,17 +269,7 @@ internal sealed class HeicDecoderCore : IImageDecoderInternals if (version >= 2) { - uint itemId = 0U; - if (version == 2) - { - itemId = BinaryPrimitives.ReadUInt16BigEndian(buffer[bytesRead..]); - bytesRead += 2; - } - else if (version == 3) - { - itemId = BinaryPrimitives.ReadUInt32BigEndian(buffer[bytesRead..]); - bytesRead += 4; - } + uint itemId = ReadUInt16Or32(buffer, version == 3, ref bytesRead); // Skip Protection Index, not sure what that means... bytesRead += 2; @@ -310,44 +302,21 @@ internal sealed class HeicDecoderCore : IImageDecoderInternals private void ParseItemReference(BufferedReadStream stream, long boxLength) { - Span buffer = new byte[boxLength]; - stream.Read(buffer); + Span buffer = this.ReadIntoBuffer(stream, boxLength); int bytesRead = 0; bool largeIds = buffer[bytesRead] != 0; bytesRead += 4; while (bytesRead < boxLength) { ParseBoxHeader(buffer[bytesRead..], out long subBoxLength, out Heic4CharCode linkType); - uint sourceId; - if (largeIds) - { - sourceId = BinaryPrimitives.ReadUInt32BigEndian(buffer[bytesRead..]); - bytesRead += 4; - } - else - { - sourceId = BinaryPrimitives.ReadUInt16BigEndian(buffer[bytesRead..]); - bytesRead += 2; - } - + uint sourceId = ReadUInt16Or32(buffer, largeIds, ref bytesRead); HeicItemLink link = new(linkType, sourceId); int count = BinaryPrimitives.ReadUInt16BigEndian(buffer[bytesRead..]); bytesRead += 2; for (uint i = 0; i < count; i++) { - uint destId; - if (largeIds) - { - destId = BinaryPrimitives.ReadUInt32BigEndian(buffer[bytesRead..]); - bytesRead += 4; - } - else - { - destId = BinaryPrimitives.ReadUInt16BigEndian(buffer[bytesRead..]); - bytesRead += 2; - } - + uint destId = ReadUInt16Or32(buffer, largeIds, ref bytesRead); link.DestinationIds.Add(destId); } @@ -358,17 +327,10 @@ internal sealed class HeicDecoderCore : IImageDecoderInternals private void ParsePrimaryItem(BufferedReadStream stream, long boxLength) { // BoxLength should be 6 or 8. - Span buffer = stackalloc byte[(int)boxLength]; - stream.Read(buffer); + Span buffer = this.ReadIntoBuffer(stream, boxLength); byte version = buffer[0]; - if (version == 0) - { - this.primaryItem = BinaryPrimitives.ReadUInt16BigEndian(buffer[4..]); - } - else - { - this.primaryItem = BinaryPrimitives.ReadUInt32BigEndian(buffer[4..]); - } + int bytesRead = 0; + this.primaryItem = ReadUInt16Or32(buffer, version != 0, ref bytesRead); } private void ParseItemPropertyContainer(BufferedReadStream stream, long boxLength) @@ -383,28 +345,21 @@ internal sealed class HeicDecoderCore : IImageDecoderInternals while (stream.Position < endPosition) { int length = (int)this.ReadBoxHeader(stream, out Heic4CharCode boxType); - Span buffer = stackalloc byte[length]; + Span buffer = this.ReadIntoBuffer(stream, boxLength); switch (boxType) { case Heic4CharCode.ispe: - // Length should be 12. - stream.Read(buffer); - // Skip over version (8 bits) and flags (24 bits). int width = (int)BinaryPrimitives.ReadUInt32BigEndian(buffer[4..]); int height = (int)BinaryPrimitives.ReadUInt32BigEndian(buffer[8..]); properties.Add(new KeyValuePair(Heic4CharCode.ispe, new Size(width, height))); break; case Heic4CharCode.pasp: - // Length should be 8. - stream.Read(buffer); int horizontalSpacing = (int)BinaryPrimitives.ReadUInt32BigEndian(buffer); int verticalSpacing = (int)BinaryPrimitives.ReadUInt32BigEndian(buffer[4..]); properties.Add(new KeyValuePair(Heic4CharCode.pasp, new Size(horizontalSpacing, verticalSpacing))); break; case Heic4CharCode.pixi: - stream.Read(buffer); - // Skip over version (8 bits) and flags (24 bits). int channelCount = buffer[4]; int offset = 5; @@ -424,6 +379,7 @@ internal sealed class HeicDecoderCore : IImageDecoderInternals case Heic4CharCode.rloc: case Heic4CharCode.udes: // TODO: Implement + SkipBox(stream, length); break; default: throw new ImageFormatException($"Unknown item property box type of '{Enum.GetName(boxType)}'"); @@ -433,21 +389,11 @@ internal sealed class HeicDecoderCore : IImageDecoderInternals else if (containerType == Heic4CharCode.ipma) { // Parse Item Property Association - Span buffer = stackalloc byte[(int)boxLength]; + Span buffer = this.ReadIntoBuffer(stream, boxLength); byte version = buffer[0]; byte flags = buffer[3]; - int itemId; int bytesRead = 4; - if (version < 1) - { - itemId = BinaryPrimitives.ReadUInt16BigEndian(buffer[bytesRead..]); - bytesRead += 2; - } - else - { - itemId = (int)BinaryPrimitives.ReadUInt32BigEndian(buffer[bytesRead..]); - bytesRead += 4; - } + int itemId = (int)ReadUInt16Or32(buffer, version >= 1, ref bytesRead); int associationCount = buffer[bytesRead++]; for (int i = 0; i < associationCount; i++) @@ -482,9 +428,115 @@ internal sealed class HeicDecoderCore : IImageDecoderInternals } } + private void ParseItemLocation(BufferedReadStream stream, long boxLength) + { + Span buffer = this.ReadIntoBuffer(stream, boxLength); + int bytesRead = 0; + byte version = buffer[bytesRead++]; + byte b1 = buffer[bytesRead++]; + byte b2 = buffer[bytesRead++]; + int offsetSize = (b1 >> 4) & 0x0f; + int lengthSize = b1 & 0x0f; + int baseOffsetSize = (b2 >> 4) & 0x0f; + int indexSize = 0; + if (version is 1 or 2) + { + indexSize = b2 & 0x0f; + } + + uint itemCount = ReadUInt16Or32(buffer, version >= 2, ref bytesRead); + for (uint i = 0; i < itemCount; i++) + { + uint itemId = ReadUInt16Or32(buffer, version >= 2, ref bytesRead); + if (version is 1 or 2) + { + bytesRead += 2; + byte b3 = buffer[bytesRead++]; + int constructionMethod = b3 & 0x0f; + } + + uint dataReferenceIndex = BinaryPrimitives.ReadUInt16BigEndian(buffer[bytesRead..]); + bytesRead += 2; + ulong baseOffset = ReadUIntVariable(buffer, baseOffsetSize, ref bytesRead); + uint extentCount = BinaryPrimitives.ReadUInt16BigEndian(buffer[bytesRead..]); + bytesRead += 2; + for (uint j = 0; j < extentCount; j++) + { + if (version is 1 or 2 && indexSize > 0) + { + ulong extentIndex = ReadUIntVariable(buffer, indexSize, ref bytesRead); + } + + ulong extentOffset = ReadUIntVariable(buffer, offsetSize, ref bytesRead); + ulong extentLength = ReadUIntVariable(buffer, lengthSize, ref bytesRead); + } + } + } + + private static uint ReadUInt16Or32(Span buffer, bool isLarge, ref int bytesRead) + { + uint result; + if (isLarge) + { + result = BinaryPrimitives.ReadUInt32BigEndian(buffer[bytesRead..]); + bytesRead += 4; + } + else + { + result = BinaryPrimitives.ReadUInt16BigEndian(buffer[bytesRead..]); + bytesRead += 2; + } + + return result; + } + + private static ulong ReadUIntVariable(Span buffer, int numBytes, ref int bytesRead) + { + ulong result = 0UL; + if (numBytes == 8) + { + result = BinaryPrimitives.ReadUInt64BigEndian(buffer[bytesRead..]); + bytesRead += 8; + } + else if (numBytes == 4) + { + result = BinaryPrimitives.ReadUInt32BigEndian(buffer[bytesRead..]); + bytesRead += 4; + } + else if (numBytes == 2) + { + result = BinaryPrimitives.ReadUInt16BigEndian(buffer[bytesRead..]); + bytesRead += 2; + } + else if (numBytes == 1) + { + result = buffer[bytesRead++]; + } + + return result; + } + private void ParseMediaData(BufferedReadStream stream, long boxLength) { - // TODO: Implement + SkipBox(stream, boxLength); + } + + private static void SkipBox(BufferedReadStream stream, long boxLength) + => stream.Skip((int)boxLength); + + private Span ReadIntoBuffer(BufferedReadStream stream, long length) + { + if (length <= this.buffer.Length) + { + stream.Read(this.buffer, 0, (int)length); + return this.buffer; + } + else + { + Span temp = new byte[length]; + stream.Read(temp); + return temp; + } } private HeicItem? FindItemById(uint itemId) diff --git a/src/ImageSharp/Formats/Heic/HeicItem.cs b/src/ImageSharp/Formats/Heic/HeicItem.cs index 654eecd53d..481ed04860 100644 --- a/src/ImageSharp/Formats/Heic/HeicItem.cs +++ b/src/ImageSharp/Formats/Heic/HeicItem.cs @@ -68,6 +68,13 @@ public class HeicItem(Heic4CharCode type, uint id) /// public Size GridCellExtent { get; private set; } + /// + /// Set the image extent. + /// + /// The size to set the extent to. + /// + /// Might be called twice for a grid, in which case the second call is the cell extent. + /// public void SetExtent(Size extent) { if (this.Extent == default) @@ -79,5 +86,4 @@ public class HeicItem(Heic4CharCode type, uint id) this.GridCellExtent = extent; } } - }