Browse Source

Apply HEIF image orientation properties

pull/2633/head
James Jackson-South 2 months ago
parent
commit
472b9c20b0
  1. 4
      HEIF_IMPLEMENTATION_PLAN.md
  2. 100
      src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
  3. 12
      src/ImageSharp/Formats/Heif/HeifItem.cs

4
HEIF_IMPLEMENTATION_PLAN.md

@ -85,6 +85,7 @@ This snapshot pins or classifies the available references and failures; it does
| `Av1InverseQuantizer` and `Av1InverseQuantizationLookup` | AV1 section 7.12.3 inverse quantization | libaom `aom_dsp/aom_dsp_common.h`, `av1/common/quant_common.c`, and `av1/decoder/decodetxb.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Select the per-segment matrix level, alias 64-pixel transform dimensions to their adjusted matrices, retain a flat level-15 matrix, and apply the five-bit inverse-matrix weight scale. The large managed lookup remains a single process-wide table. | | `Av1InverseQuantizer` and `Av1InverseQuantizationLookup` | AV1 section 7.12.3 inverse quantization | libaom `aom_dsp/aom_dsp_common.h`, `av1/common/quant_common.c`, and `av1/decoder/decodetxb.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Select the per-segment matrix level, alias 64-pixel transform dimensions to their adjusted matrices, retain a flat level-15 matrix, and apply the five-bit inverse-matrix weight scale. The large managed lookup remains a single process-wide table. |
| `Av1Inverse2dTransformer` and `Av1InverseTransformerFactory` | AV1 section 7.11.2 inverse transform and reconstruction | libaom `av1/common/av1_inv_txfm1d.c`, `av1/common/av1_inv_txfm2d.c`, and `av1/common/idct.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Scalar transform oracle for coefficient-row traversal, intermediate layout, stage ranges, clipping, and high-bit-depth sample addition. The managed 16-bit overload is also used as a parity oracle for the byte overload. | | `Av1Inverse2dTransformer` and `Av1InverseTransformerFactory` | AV1 section 7.11.2 inverse transform and reconstruction | libaom `av1/common/av1_inv_txfm1d.c`, `av1/common/av1_inv_txfm2d.c`, and `av1/common/idct.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Scalar transform oracle for coefficient-row traversal, intermediate layout, stage ranges, clipping, and high-bit-depth sample addition. The managed 16-bit overload is also used as a parity oracle for the byte overload. |
| `HeifDecoderCore` box extension handling and `HeifDecoderCore`/`HeifEncoderCore` item-property associations | ISO/IEC 14496-12 box extensibility and section 8.11.14 item properties and `ipma` syntax | libavif `src/read.c` and `src/write.c` at `092276ce89098ead06db80975173191e5fee1826` | Skip unrecognized top-level and metadata child boxes, preserve the position of every property in `ipco`, reject an unrecognized property only when its item association marks it essential, associate properties by item ID, and read or write the essential bit plus one-based 7-bit or 15-bit property index according to the full-box flags. Independent HEIC, HIF, and AVIF fixtures provide the reader oracle; container-level identification of encoded output guards the writer independently of pixel roundtripping. | | `HeifDecoderCore` box extension handling and `HeifDecoderCore`/`HeifEncoderCore` item-property associations | ISO/IEC 14496-12 box extensibility and section 8.11.14 item properties and `ipma` syntax | libavif `src/read.c` and `src/write.c` at `092276ce89098ead06db80975173191e5fee1826` | Skip unrecognized top-level and metadata child boxes, preserve the position of every property in `ipco`, reject an unrecognized property only when its item association marks it essential, associate properties by item ID, and read or write the essential bit plus one-based 7-bit or 15-bit property index according to the full-box flags. Independent HEIC, HIF, and AVIF fixtures provide the reader oracle; container-level identification of encoded output guards the writer independently of pixel roundtripping. |
| `HeifItem` rotation/mirror state and `HeifDecoderCore` presentation-transform parsing and application | HEIF image rotation and mirror properties; MIAF section 7.3.6.7 presentation order and section 7.3.9 essential transformative properties | libavif `src/read.c` property parsers and alpha-property validation plus `apps/shared/avifutil.c` transform application at `092276ce89098ead06db80975173191e5fee1826` | Validate the registered reserved bits, require essential associations, map counter-clockwise HEIF quarter turns to ImageSharp's optimized clockwise rotate modes, then mirror around the signaled axis after alpha composition. Reuse ImageSharp's existing rotation and flip processors for every pixel type. Retain only the two image-item property values; do not add a generic transform-box or ISO BMFF model. |
| `HeifConstants.IsSupportedFileType`, `HeifImageFormatDetector`, and `HeifDecoderCore.CheckFileTypeBox` | ISO/IEC 14496-12 `FileTypeBox` syntax and the MP4 Registration Authority HEIF/AVIF still-image and sequence brand registrations | libavif `src/read.c` functions `avifParseFileTypeBox`, `avifFileTypeHasBrand`, and `avifFileTypeIsCompatible` at `092276ce89098ead06db80975173191e5fee1826` | Apply one rule to the major and compatible brands, accept the implemented still-image container and payload brands, and reject registered HEVC, AVIF, and JPEG sequence major brands as outside the image-item scope. The decoder validates the complete `ftyp` payload; the fixed-size format detector inspects the available prefix. | | `HeifConstants.IsSupportedFileType`, `HeifImageFormatDetector`, and `HeifDecoderCore.CheckFileTypeBox` | ISO/IEC 14496-12 `FileTypeBox` syntax and the MP4 Registration Authority HEIF/AVIF still-image and sequence brand registrations | libavif `src/read.c` functions `avifParseFileTypeBox`, `avifFileTypeHasBrand`, and `avifFileTypeIsCompatible` at `092276ce89098ead06db80975173191e5fee1826` | Apply one rule to the major and compatible brands, accept the implemented still-image container and payload brands, and reject registered HEVC, AVIF, and JPEG sequence major brands as outside the image-item scope. The decoder validates the complete `ftyp` payload; the fixed-size format detector inspects the available prefix. |
| `HeifDecoderCore.ReadBoxHeader` and `HeifDecoderCore.ParseBoxHeader` | ISO/IEC 14496-12 section 4.2.2 basic box syntax | libavif `src/stream.c` functions `avifROStreamReadBoxHeaderPartial` and `avifROStreamReadBoxHeader` at `092276ce89098ead06db80975173191e5fee1826` | Resolve 32-bit, 64-bit, UUID, and top-level size-zero boxes into content lengths only after validating the complete variable-sized header and the remaining parent boundary. Nested size-zero boxes are invalid; large skips retain 64-bit offsets. | | `HeifDecoderCore.ReadBoxHeader` and `HeifDecoderCore.ParseBoxHeader` | ISO/IEC 14496-12 section 4.2.2 basic box syntax | libavif `src/stream.c` functions `avifROStreamReadBoxHeaderPartial` and `avifROStreamReadBoxHeader` at `092276ce89098ead06db80975173191e5fee1826` | Resolve 32-bit, 64-bit, UUID, and top-level size-zero boxes into content lengths only after validating the complete variable-sized header and the remaining parent boundary. Nested size-zero boxes are invalid; large skips retain 64-bit offsets. |
| `HeifDecoderCore.ParseMetadata` | ISO/IEC 14496-12 `MetaBox` and HEIF item declarations, locations, properties, and associations | libavif `src/read.c` functions `avifParseMetaBox`, `avifMetaFindOrCreateItem`, `avifParseItemLocationBox`, and `avifParseItemPropertiesBox` at `092276ce89098ead06db80975173191e5fee1826` | Index unique recognized metadata children by type and payload location, then parse them in dependency order so physical placement does not control item lookup or property association. Duplicate unique children and truncated full-box headers are invalid. | | `HeifDecoderCore.ParseMetadata` | ISO/IEC 14496-12 `MetaBox` and HEIF item declarations, locations, properties, and associations | libavif `src/read.c` functions `avifParseMetaBox`, `avifMetaFindOrCreateItem`, `avifParseItemLocationBox`, and `avifParseItemPropertiesBox` at `092276ce89098ead06db80975173191e5fee1826` | Index unique recognized metadata children by type and payload location, then parse them in dependency order so physical placement does not control item lookup or property association. Duplicate unique children and truncated full-box headers are invalid. |
@ -115,7 +116,8 @@ This assessment is based on the current source after the upstream ImageSharp mer
- Item locations now support bounded file-relative and `idat`-relative storage, multiple ordered extents, versioned item IDs, 0/4/8-byte registered field sizes, and 64-bit offsets. Referenced-item construction method two and external data references remain explicitly unsupported. - Item locations now support bounded file-relative and `idat`-relative storage, multiple ordered extents, versioned item IDs, 0/4/8-byte registered field sizes, and 64-bit offsets. Referenced-item construction method two and external data references remain explicitly unsupported.
- Grid derived-image decoding now parses both registered descriptor widths, resolves the ordered `dimg` cells, validates tile count, coding format, dimensions, canvas coverage, and edge overlap, then composes the output through ImageSharp row buffers. Unsupported grid tile codecs can select only a decodable thumbnail of the same primary grid. Independent AV1 and JPEG grid fixtures are still required, and HEVC grids remain blocked on the HEVC decoder. - Grid derived-image decoding now parses both registered descriptor widths, resolves the ordered `dimg` cells, validates tile count, coding format, dimensions, canvas coverage, and edge overlap, then composes the output through ImageSharp row buffers. Unsupported grid tile codecs can select only a decodable thumbnail of the same primary grid. Independent AV1 and JPEG grid fixtures are still required, and HEVC grids remain blocked on the HEVC decoder.
- Alpha auxiliary decoding now recognizes `auxC`, `auxl`, and `prem`, supports a direct auxiliary image/grid and the per-color-grid-tile form, preserves normalized alpha through `L16`/`Rgba64`, box-resamples differing plane sizes, and reports alpha presence. HEVC alpha remains blocked on the HEVC image-item decoder, while independent AVIF alpha fixtures are still required against the incomplete AV1 reconstruction pipeline. - Alpha auxiliary decoding now recognizes `auxC`, `auxl`, and `prem`, supports a direct auxiliary image/grid and the per-color-grid-tile form, preserves normalized alpha through `L16`/`Rgba64`, box-resamples differing plane sizes, and reports alpha presence. HEVC alpha remains blocked on the HEVC image-item decoder, while independent AVIF alpha fixtures are still required against the incomplete AV1 reconstruction pipeline.
- HEVC and AV1 configuration, CICP color information, Exif/XMP, transforms, and several image-item properties/relationships are missing or parsed without affecting output. - Image rotation and mirror properties now validate their registered payloads and essential associations, affect Identify dimensions, and reuse ImageSharp's optimized rotate/flip processors after auxiliary alpha composition in the MIAF-defined order. Clean aperture remains missing, and independent transform vectors must still verify every rotation/mirror/alpha combination.
- HEVC and AV1 configuration, CICP color information, Exif/XMP, clean aperture, and several image-item properties/relationships are missing or parsed without affecting output.
- Identify and decode now use the same bounded metadata parser and both validate the complete leading file type box. The parsed state is still mutable and Identify does not yet report the complete bit depth, color, profile, or transform model. - Identify and decode now use the same bounded metadata parser and both validate the complete leading file type box. The parsed state is still mutable and Identify does not yet report the complete bit depth, color, profile, or transform model.
### HEVC decoder and encoder ### HEVC decoder and encoder

100
src/ImageSharp/Formats/Heif/HeifDecoderCore.cs

@ -170,7 +170,8 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
this.UpdateMetadata(this.metadata, item); this.UpdateMetadata(this.metadata, item);
return new ImageInfo(new(item.Extent.Width, item.Extent.Height), this.metadata); Size presentationExtent = GetPresentationExtent(item);
return new ImageInfo(new(presentationExtent.Width, presentationExtent.Height), this.metadata);
} }
/// <summary> /// <summary>
@ -843,9 +844,25 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
this.av1CodecConfiguration = new(boxBuffer); this.av1CodecConfiguration = new(boxBuffer);
properties.Add(new KeyValuePair<Heif4CharCode, object>(Heif4CharCode.Av1C, new object())); properties.Add(new KeyValuePair<Heif4CharCode, object>(Heif4CharCode.Av1C, new object()));
break; break;
case Heif4CharCode.Altt:
case Heif4CharCode.Imir:
case Heif4CharCode.Irot: case Heif4CharCode.Irot:
EnsureBufferRemaining(boxBuffer, 0, 1, "image rotation");
if ((boxBuffer[0] & 0xFC) != 0)
{
throw new InvalidImageContentException("The image rotation property has nonzero reserved bits.");
}
properties.Add(new KeyValuePair<Heif4CharCode, object>(Heif4CharCode.Irot, (byte)(boxBuffer[0] & 3)));
break;
case Heif4CharCode.Imir:
EnsureBufferRemaining(boxBuffer, 0, 1, "image mirror");
if ((boxBuffer[0] & 0xFE) != 0)
{
throw new InvalidImageContentException("The image mirror property has nonzero reserved bits.");
}
properties.Add(new KeyValuePair<Heif4CharCode, object>(Heif4CharCode.Imir, (byte)(boxBuffer[0] & 1)));
break;
case Heif4CharCode.Altt:
case Heif4CharCode.Iscl: case Heif4CharCode.Iscl:
case Heif4CharCode.HvcC: case Heif4CharCode.HvcC:
case Heif4CharCode.Rloc: case Heif4CharCode.Rloc:
@ -939,6 +956,11 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
throw new InvalidImageContentException($"Item {itemId} associates unknown essential property '{PrettyPrint(prop.Key)}'."); throw new InvalidImageContentException($"Item {itemId} associates unknown essential property '{PrettyPrint(prop.Key)}'.");
} }
if (!essential && prop.Key is Heif4CharCode.Irot or Heif4CharCode.Imir)
{
throw new InvalidImageContentException($"Item {itemId} associates nonessential transformative property '{PrettyPrint(prop.Key)}'.");
}
switch (prop.Key) switch (prop.Key)
{ {
case Heif4CharCode.Ispe: case Heif4CharCode.Ispe:
@ -960,6 +982,22 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
item.AuxiliaryType = (string)prop.Value; item.AuxiliaryType = (string)prop.Value;
break; break;
case Heif4CharCode.Irot:
if (item.RotationAngle is not null)
{
throw new InvalidImageContentException($"Item {itemId} associates more than one image rotation property.");
}
item.RotationAngle = (byte)prop.Value;
break;
case Heif4CharCode.Imir:
if (item.MirrorAxis is not null)
{
throw new InvalidImageContentException($"Item {itemId} associates more than one image mirror property.");
}
item.MirrorAxis = (byte)prop.Value;
break;
} }
} }
} }
@ -1257,6 +1295,10 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
this.ApplyAlpha(image, alphaImage, alphaPremultiplied); this.ApplyAlpha(image, alphaImage, alphaPremultiplied);
} }
// MIAF defines crop, rotation, and mirror as presentation operations in that order. Applying the
// implemented transforms after alpha composition keeps the auxiliary plane in the same coordinate space.
ApplyPresentationTransforms(image, itemToDecode);
// The decoder determines the compression of the pixels that were actually returned, including grid tiles // The decoder determines the compression of the pixels that were actually returned, including grid tiles
// and a thumbnail fallback when the primary image compression is not available. // and a thumbnail fallback when the primary image compression is not available.
HeifMetadata meta = image.Metadata.GetHeifMetadata(); HeifMetadata meta = image.Metadata.GetHeifMetadata();
@ -1317,6 +1359,50 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
} }
} }
/// <summary>
/// Gets the dimensions of an image item after its rotation property is applied.
/// </summary>
/// <param name="item">The image item whose presentation dimensions are requested.</param>
/// <returns>The item dimensions after an optional quarter-turn rotation.</returns>
private static Size GetPresentationExtent(HeifItem item)
=> item.RotationAngle is not null && (item.RotationAngle.Value & 1) != 0
? new Size(item.Extent.Height, item.Extent.Width)
: item.Extent;
/// <summary>
/// Applies the rotation and mirror properties associated with an image item.
/// </summary>
/// <typeparam name="TPixel">The image pixel format.</typeparam>
/// <param name="image">The decoded image item.</param>
/// <param name="item">The item carrying the presentation properties.</param>
private static void ApplyPresentationTransforms<TPixel>(Image<TPixel> image, HeifItem item)
where TPixel : unmanaged, IPixel<TPixel>
{
if (item.RotationAngle is not null)
{
// HEIF angles count quarter turns counter-clockwise, while ImageSharp's optimized rotate modes are clockwise.
RotateMode rotation = item.RotationAngle.Value switch
{
1 => RotateMode.Rotate270,
2 => RotateMode.Rotate180,
3 => RotateMode.Rotate90,
_ => RotateMode.None
};
if (rotation != RotateMode.None)
{
image.Mutate(context => context.Rotate(rotation));
}
}
if (item.MirrorAxis is not null)
{
// Axis zero reflects top-to-bottom around the horizontal axis; axis one reflects left-to-right.
FlipMode flip = item.MirrorAxis.Value == 0 ? FlipMode.Vertical : FlipMode.Horizontal;
image.Mutate(context => context.Flip(flip));
}
}
/// <summary> /// <summary>
/// Decodes the direct or per-grid-tile alpha auxiliary plane associated with a color image item. /// Decodes the direct or per-grid-tile alpha auxiliary plane associated with a color image item.
/// </summary> /// </summary>
@ -1333,6 +1419,14 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
HeifItem? alphaItem = this.FindAlphaItem(colorItem); HeifItem? alphaItem = this.FindAlphaItem(colorItem);
if (alphaItem is not null) if (alphaItem is not null)
{ {
// libavif releases through 1.3 omitted alpha transform associations, so accept complete absence for
// compatibility. If either property is present, it must match the color item before plane composition.
if ((alphaItem.RotationAngle is not null || alphaItem.MirrorAxis is not null) &&
(alphaItem.RotationAngle != colorItem.RotationAngle || alphaItem.MirrorAxis != colorItem.MirrorAxis))
{
throw new ImageFormatException("The alpha auxiliary image and color image use different presentation transforms.");
}
IHeifItemDecoder<L16>? decoder = this.GetItemDecoder<L16>(alphaItem, buffers); IHeifItemDecoder<L16>? decoder = this.GetItemDecoder<L16>(alphaItem, buffers);
if (decoder is null) if (decoder is null)
{ {

12
src/ImageSharp/Formats/Heif/HeifItem.cs

@ -55,6 +55,18 @@ internal class HeifItem(Heif4CharCode type, uint id)
/// </summary> /// </summary>
public Size PixelAspectRatio { get; set; } public Size PixelAspectRatio { get; set; }
/// <summary>
/// Gets or sets the number of 90-degree counter-clockwise rotations applied to the image, or
/// <see langword="null"/> when no image-rotation property is associated with the item.
/// </summary>
public byte? RotationAngle { get; set; }
/// <summary>
/// Gets or sets the image-mirror axis, where zero is the horizontal axis and one is the vertical axis, or
/// <see langword="null"/> when no image-mirror property is associated with the item.
/// </summary>
public byte? MirrorAxis { get; set; }
/// <summary> /// <summary>
/// Gets or sets the number of color channels in each pixel. /// Gets or sets the number of color channels in each pixel.
/// </summary> /// </summary>

Loading…
Cancel
Save