diff --git a/src/ImageSharp/Formats/Heic/AutoExpandingMemory.cs b/src/ImageSharp/Formats/Heic/AutoExpandingMemory.cs new file mode 100644 index 0000000000..376654a205 --- /dev/null +++ b/src/ImageSharp/Formats/Heic/AutoExpandingMemory.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; + +namespace SixLabors.ImageSharp.Formats.Heic; + +/// +/// Memory class that will expand dynamically when full. +/// +internal sealed class AutoExpandingMemory : IDisposable + where T : unmanaged +{ + private readonly Configuration configuration; + private IMemoryOwner allocation; + + public AutoExpandingMemory(Configuration configuration, int initialSize) + { + Guard.MustBeGreaterThan(initialSize, 0, nameof(initialSize)); + + this.configuration = configuration; + this.allocation = this.configuration.MemoryAllocator.Allocate(initialSize); + } + + public Span GetSpan(int requestedSize) + { + Guard.MustBeGreaterThan(requestedSize, 0, nameof(requestedSize)); + this.EnsureCapacity(requestedSize); + + return this.allocation.Memory.Span[..requestedSize]; + } + + public Span GetSpan(int offset, int requestedSize) + { + Guard.MustBeGreaterThan(offset, 0, nameof(offset)); + Guard.MustBeGreaterThan(requestedSize, 0, nameof(requestedSize)); + this.EnsureCapacity(offset + requestedSize); + + return this.allocation.Memory.Span.Slice(offset, requestedSize); + } + + public void Dispose() => this.allocation.Dispose(); + + private void EnsureCapacity(int requestedSize) + { + if (requestedSize > this.allocation.Memory.Length) + { + int newSize = requestedSize + (requestedSize / 5); + IMemoryOwner newAllocation = this.configuration.MemoryAllocator.Allocate(newSize); + this.allocation.Memory.CopyTo(newAllocation.Memory); + this.allocation.Dispose(); + this.allocation = newAllocation; + } + } +} diff --git a/src/ImageSharp/Formats/Heic/Heic4CharCode.cs b/src/ImageSharp/Formats/Heic/Heic4CharCode.cs index 42867059dd..5e184a26e0 100644 --- a/src/ImageSharp/Formats/Heic/Heic4CharCode.cs +++ b/src/ImageSharp/Formats/Heic/Heic4CharCode.cs @@ -113,6 +113,11 @@ public enum Heic4CharCode : uint /// udes = 0x75646573U, + /// + /// IPMP Control Box. + /// + ipmc = 0x69706D63U, + /// /// Item Property Container. /// @@ -136,7 +141,7 @@ public enum Heic4CharCode : uint /// /// High Efficient File brand. /// - hif1 = 0x68696631U, + mif1 = 0x6D696631U, /// /// High Efficiency Coding tile. diff --git a/src/ImageSharp/Formats/Heic/Heic4CharCode.tt b/src/ImageSharp/Formats/Heic/Heic4CharCode.tt index 1bcbc8312a..7faca67008 100644 --- a/src/ImageSharp/Formats/Heic/Heic4CharCode.tt +++ b/src/ImageSharp/Formats/Heic/Heic4CharCode.tt @@ -27,11 +27,12 @@ "pixi", "Pixel Information", "rloc", "Reference Location", "udes", "User Description", + "ipmc", "IPMP Control Box", "ipco", "Item Property Container", "ipma", "Item Property Association", "heic", "High Efficient Image Coding brand", "heix", "High Efficient Image Coding brand (legacy name)", - "hif1", "High Efficient File brand", + "mif1", "High Efficient File brand", "hvc1", "High Efficiency Coding tile", "jpeg", "Legacy JPEG coded tile", "dinf", "Data Information", diff --git a/src/ImageSharp/Formats/Heic/HeicDecoderCore.cs b/src/ImageSharp/Formats/Heic/HeicDecoderCore.cs index ed2381cc62..2a111ad0b3 100644 --- a/src/ImageSharp/Formats/Heic/HeicDecoderCore.cs +++ b/src/ImageSharp/Formats/Heic/HeicDecoderCore.cs @@ -219,6 +219,7 @@ internal sealed class HeicDecoderCore : IImageDecoderInternals case Heic4CharCode.grpl: case Heic4CharCode.ipro: case Heic4CharCode.uuid: + case Heic4CharCode.ipmc: // Silently skip these boxes. SkipBox(stream, length); break; diff --git a/src/ImageSharp/Formats/Heic/HeicEncoderCore.cs b/src/ImageSharp/Formats/Heic/HeicEncoderCore.cs index 55f892a8ce..e028cde1a6 100644 --- a/src/ImageSharp/Formats/Heic/HeicEncoderCore.cs +++ b/src/ImageSharp/Formats/Heic/HeicEncoderCore.cs @@ -1,8 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers.Text; -using SixLabors.ImageSharp.Advanced; +using System; +using System.Buffers.Binary; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Heic; @@ -40,13 +42,288 @@ internal sealed class HeicEncoderCore : IImageEncoderInternals /// The to encode from. /// The to encode the image data to. /// The token to request cancellation. - public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + public async void Encode(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - // TODO: Implement + byte[] pixels = await CompressPixels(image, cancellationToken); + List items = new(); + List links = new(); + GenerateItems(image, pixels, items, links); + + // Write out the generated header and pixels. + this.WriteFileTypeBox(stream); + this.WriteMetadataBox(items, links, stream); + this.WriteMediaDataBox(pixels, stream); stream.Flush(); } + + private static void GenerateItems(Image image, byte[] pixels, List items, List links) + where TPixel : unmanaged, IPixel + { + HeicItem primaryItem = new(Heic4CharCode.jpeg, 1u); + primaryItem.DataLocations.Add(new HeicLocation(0L, pixels.LongLength)); + primaryItem.BitsPerPixel = 24; + primaryItem.ChannelCount = 3; + primaryItem.SetExtent(image.Size); + + // Create a fake thumbnail, to make our own Decoder happy. + HeicItemLink thumbnail = new(Heic4CharCode.thmb, 1u); + thumbnail.DestinationIds.Add(1u); + } + + private static int WriteBoxHeader(Span buffer, Heic4CharCode type) + { + int bytesWritten = 0; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], 8U); + bytesWritten += 4; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)type); + bytesWritten += 4; + + return bytesWritten; + } + + private static int WriteBoxHeader(Span buffer, Heic4CharCode type, byte version, uint flags) + { + int bytesWritten = 0; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], 12U); + bytesWritten += 4; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)type); + bytesWritten += 4; + + // Layout in memory is 4 bytes, 1 version byte followed by 3 flag bytes. + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], flags); + buffer[bytesWritten] = version; + bytesWritten += 4; + + return bytesWritten; + } + + private void WriteFileTypeBox(Stream stream) + { + Span buffer = stackalloc byte[24]; + int bytesWritten = WriteBoxHeader(buffer, Heic4CharCode.ftyp); + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heic4CharCode.heic); + bytesWritten += 4; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], 0U); + bytesWritten += 4; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heic4CharCode.mif1); + bytesWritten += 4; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heic4CharCode.heic); + bytesWritten += 4; + + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); + stream.Write(buffer); + } + + private void WriteMetadataBox(List items, List links, Stream stream) + { + using AutoExpandingMemory memory = new(this.configuration, 0x1000); + Span buffer = memory.GetSpan(12); + int bytesWritten = WriteBoxHeader(buffer, Heic4CharCode.meta, 0, 0); + bytesWritten += WriteHandlerBox(memory, bytesWritten); + bytesWritten += WritePrimaryItemBox(memory, bytesWritten); + bytesWritten += WriteItemInfoBox(memory, bytesWritten, items); + bytesWritten += WriteItemReferenceBox(memory, bytesWritten, links); + bytesWritten += WriteItemPropertiesBox(memory, bytesWritten, items); + bytesWritten += WriteItemDataBox(memory, bytesWritten); + bytesWritten += WriteItemLocationBox(memory, bytesWritten, items); + + buffer = memory.GetSpan(bytesWritten); + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); + stream.Write(buffer); + } + + private static int WriteHandlerBox(AutoExpandingMemory memory, int memoryOffset) + { + Span buffer = memory.GetSpan(memoryOffset, 100); + int bytesWritten = WriteBoxHeader(buffer, Heic4CharCode.hdlr, 0, 0); + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], 0); + bytesWritten += 4; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heic4CharCode.pict); + bytesWritten += 4; + for (int i = 0; i < 13; i++) + { + buffer[bytesWritten++] = 0; + } + + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); + return bytesWritten; + } + + private static int WritePrimaryItemBox(AutoExpandingMemory memory, int memoryOffset) + { + Span buffer = memory.GetSpan(memoryOffset, 100); + int bytesWritten = WriteBoxHeader(buffer, Heic4CharCode.pitm, 0, 0); + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], 1); + bytesWritten += 2; + + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); + return bytesWritten; + } + + private static int WriteItemInfoBox(AutoExpandingMemory memory, int memoryOffset, List items) + { + Span buffer = memory.GetSpan(memoryOffset, 100); + int bytesWritten = WriteBoxHeader(buffer, Heic4CharCode.iinf, 0, 0); + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)items.Count); + bytesWritten += 2; + foreach (HeicItem item in items) + { + int itemLengthOffset = bytesWritten; + bytesWritten += WriteBoxHeader(buffer[bytesWritten..], Heic4CharCode.infe, 2, 0); + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)item.Id); + bytesWritten += 2; + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], 0); + bytesWritten += 2; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)item.Type); + bytesWritten += 4; + buffer[bytesWritten++] = 0; + + BinaryPrimitives.WriteUInt32BigEndian(buffer[itemLengthOffset..], (uint)(bytesWritten - itemLengthOffset)); + } + + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); + return bytesWritten; + } + + private static int WriteItemReferenceBox(AutoExpandingMemory memory, int memoryOffset, List links) + { + Span buffer = memory.GetSpan(memoryOffset, 100); + int bytesWritten = WriteBoxHeader(buffer, Heic4CharCode.iref, 0, 0); + foreach (HeicItemLink link in links) + { + int itemLengthOffset = bytesWritten; + bytesWritten += WriteBoxHeader(buffer[bytesWritten..], link.Type); + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)link.SourceId); + bytesWritten += 2; + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)link.DestinationIds.Count); + bytesWritten += 2; + foreach (uint destId in link.DestinationIds) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)destId); + bytesWritten += 2; + } + + BinaryPrimitives.WriteUInt32BigEndian(buffer[itemLengthOffset..], (uint)(bytesWritten - itemLengthOffset)); + } + + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); + return bytesWritten; + } + + private static int WriteItemPropertiesBox(AutoExpandingMemory memory, int memoryOffset, List items) + { + const ushort numPropPerItem = 1; + Span buffer = memory.GetSpan(memoryOffset, 100); + int bytesWritten = WriteBoxHeader(buffer, Heic4CharCode.iprp); + + // Write 'ipco' box + int ipcoLengthOffset = bytesWritten; + bytesWritten += WriteBoxHeader(buffer[bytesWritten..], Heic4CharCode.ipco); + foreach (HeicItem item in items) + { + bytesWritten += WriteSpatialExtentPropertyBox(memory, ipcoLengthOffset + bytesWritten, item); + } + + BinaryPrimitives.WriteUInt32BigEndian(buffer[ipcoLengthOffset..], (uint)(bytesWritten - ipcoLengthOffset)); + + // Write 'ipma' box + int ipmaLengthOffset = bytesWritten; + bytesWritten += WriteBoxHeader(buffer[bytesWritten..], Heic4CharCode.ipma, 0, 0); + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)(items.Count * numPropPerItem)); + bytesWritten += 4; + ushort propIndex = 0; + foreach (HeicItem item in items) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)item.Id); + bytesWritten += 2; + buffer[bytesWritten++] = 1; + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], propIndex); + bytesWritten += 2; + propIndex += numPropPerItem; + } + + BinaryPrimitives.WriteUInt32BigEndian(buffer[ipmaLengthOffset..], (uint)(bytesWritten - ipmaLengthOffset)); + + // Update size of enclosing 'iprp' box. + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); + return bytesWritten; + } + + private static int WriteSpatialExtentPropertyBox(AutoExpandingMemory memory, int memoryOffset, HeicItem item) + { + Span buffer = memory.GetSpan(memoryOffset, 100); + int bytesWritten = WriteBoxHeader(buffer, Heic4CharCode.ispe); + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)item.Extent.Width); + bytesWritten += 4; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)item.Extent.Height); + bytesWritten += 4; + + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); + return bytesWritten; + } + + private static int WriteItemDataBox(AutoExpandingMemory memory, int memoryOffset) + { + Span buffer = memory.GetSpan(memoryOffset, 100); + int bytesWritten = WriteBoxHeader(buffer, Heic4CharCode.idat); + + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); + return bytesWritten; + } + + private static int WriteItemLocationBox(AutoExpandingMemory memory, int memoryOffset, List items) + { + Span buffer = memory.GetSpan(memoryOffset, 100); + int bytesWritten = WriteBoxHeader(buffer, Heic4CharCode.iloc, 1, 0); + buffer[bytesWritten++] = 0x44; + buffer[bytesWritten++] = 0; + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], 1); + bytesWritten += 2; + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)items[0].Id); + bytesWritten += 2; + for (int i = 0; i < 4; i++) + { + buffer[bytesWritten++] = 0; + } + + IEnumerable itemLocs = items.SelectMany(item => item.DataLocations).Where(loc => loc != null); + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)itemLocs.Count()); + bytesWritten += 2; + foreach (HeicLocation loc in itemLocs) + { + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)loc.Offset); + bytesWritten += 4; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)loc.Length); + bytesWritten += 4; + } + + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); + return bytesWritten; + } + + private void WriteMediaDataBox(Span data, Stream stream) + { + Span buf = stackalloc byte[12]; + int bytesWritten = WriteBoxHeader(buf, Heic4CharCode.mdat); + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)(data.Length + bytesWritten)); + stream.Write(buf[..bytesWritten]); + + stream.Write(data); + } + + private static async Task CompressPixels(Image image, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + using MemoryStream stream = new(); + JpegEncoder encoder = new() + { + ColorType = JpegEncodingColor.YCbCrRatio420 + }; + await image.SaveAsJpegAsync(stream, encoder, cancellationToken); + return stream.ToArray(); + } }