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();
+ }
}