From 5ca8f97303b92b31d24bee2c18c20047a023bd15 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 25 Aug 2026 03:55:31 +1000 Subject: [PATCH] Apply HEIF pixel aspect ratio --- HEIF_IMPLEMENTATION_PLAN.md | 4 +- .../Formats/Heif/HeifDecoderCore.cs | 59 +++++++++++++++++-- src/ImageSharp/Formats/Heif/HeifItem.cs | 5 +- .../Formats/Heif/HeifPixelAspectRatio.cs | 22 +++++++ 4 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 src/ImageSharp/Formats/Heif/HeifPixelAspectRatio.cs diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 7cbe1088c..ca2866c8a 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -91,6 +91,7 @@ This snapshot pins or classifies the available references and failures; it does | `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.ApplyAssociatedMetadata` | HEIF Annex A Exif item data, MIME metadata items, and `cdsc` item references | libavif `src/read.c` function `avifDecoderFindMetadata`, `src/exif.c` function `avifGetExifTiffHeaderOffset`, and the Exif/XMP item writing paths in `src/write.c` at `092276ce89098ead06db80975173191e5fee1826` | Resolve only metadata items whose `cdsc` reference identifies the decoded primary image, validate the Exif TIFF-header offset, and attach Exif or `application/rdf+xml` XMP through ImageSharp's existing profile types. This is a bounded still-image metadata path; it does not introduce a generic ISO BMFF metadata, media, or track model. | | `HeifDecoderCore` color-property parsing/association, `HeifItem` color profiles, and `Av1Decoder` container color override | ISO/IEC 14496-12 section 12.1.5 color information; HEIF section 6.5.5.1 color-information properties; AV1-ISOBMFF section 2.3.4 configuration semantics | libavif `src/read.c` functions `avifParseColourInformationBox`, `avifReadColorNclxProperty`, and `avifReadColorProperties`, plus `src/write.c` function `avifEncoderWriteColorProperties`, at `092276ce89098ead06db80975173191e5fee1826` | Associate at most one ICC and one `nclx` property with each presented color image item, validate ICC payloads and CICP reserved bits, expose them through ImageSharp's existing profile types, inherit a grid's CICP description only for tiles that do not declare one, and let container CICP values override the matching AV1 sequence-header fields before still-image reconstruction and YUV-to-RGB conversion. Retain only the two image color profiles; do not add a reusable color-box, sample-entry, track, or media model. | +| `HeifPixelAspectRatio`, `HeifItem.PixelAspectRatio`, and `HeifDecoderCore.ApplyItemPixelAspectRatioMetadata` | ISO/IEC 14496-12 section 12.1.4.3 pixel aspect ratio | libavif `src/read.c` function `avifParsePixelAspectRatioBox`, `src/write.c` function `avifEncoderWritePaspProperty`, and the presented-image property selection in `src/read.c` at `092276ce89098ead06db80975173191e5fee1826` | Preserve the two unsigned 32-bit relative spacings on the associated image item, reject zero or duplicate ratios, and map the displayed pixel width-to-height ratio into ImageSharp's existing unitless resolution metadata. Exchange the metadata axes after a quarter-turn presentation rotation and fall back from a derived grid to its first decodable tile only when the grid does not declare `pasp`. This remains one still-image presentation property and introduces no generic transform, sample-entry, or display model. | | `GridHeifItemDecoder` and `HeifDecoderCore` grid/thumbnail selection | ISO/IEC 23008-12 section 6.6.2.3 image-grid syntax and MIAF grid-cell constraints | libavif `src/read.c` functions `avifParseImageGridBox`, `avifDecoderDataAllocateImagePlanes`, and `avifDecoderDataCopyTileToImage` at `092276ce89098ead06db80975173191e5fee1826` | Parse version-zero 16-bit and 32-bit grid descriptors, preserve row-major `dimg` order, require the declared tile count and one coding format, validate canvas coverage and edge overlap, and crop only the rightmost column and bottom row while copying through ImageSharp pixel buffers. A primary grid whose tile codec is unavailable may use only a decodable thumbnail that explicitly references that grid. | | `HeifDecoderCore` alpha auxiliary selection/composition and `GridHeifItemDecoder` auxiliary tile ordering | ISO/IEC 23008-12 alpha auxiliary image semantics, `auxC`, `auxl`, `prem`, and per-grid-tile alpha relationships | libavif `src/read.c` functions `avifParseAuxiliaryTypeProperty`, `avifDecoderItemIsAlphaAux`, `avifMetaFindAlphaItem`, and `avifDecoderCheckAlphaProperties`, plus `src/scale.c` box-filter scaling at `092276ce89098ead06db80975173191e5fee1826` | Recognize both registered alpha URNs, decode a direct alpha image/grid or the complete row-major set of per-color-tile alpha auxiliaries, normalize through `L16`, box-resample differing auxiliary dimensions, compose through `Rgba64` and `PixelOperations`, and unassociate `prem` color samples with transparent-black handling. This remains an image-item relationship only; no track or generic media-reference model is introduced. | @@ -121,7 +122,8 @@ This assessment is based on the current source after the upstream ImageSharp mer - Clean aperture, image rotation, and image mirror properties now validate their registered payloads, exact integer crop geometry, and essential associations; affect Identify dimensions; and reuse ImageSharp's optimized crop/rotate/flip processors after auxiliary alpha composition in the MIAF-defined order. Independent transform vectors must still verify every crop/rotation/mirror/alpha combination. - Decode now resolves `cdsc`-associated Exif and `application/rdf+xml` XMP items for the primary still image, validates the declared Exif TIFF-header offset, and attaches the payloads through ImageSharp's existing profile types before presentation transforms. Independent AVIF, HEIC, and HIF metadata fixtures and Identify-time profile reporting remain required. - ICC and `nclx` CICP color properties are now associated with the presented color item instead of global parser state, validated, exposed on Decode and Identify through the existing ImageSharp profiles, and used to override matching AV1 bitstream color fields before still-image color conversion. Independent ICC/CICP fixtures, decoded AV1 bitstream-CICP fallback metadata, ICC conversion coverage, and HEVC integration remain required. -- HEVC and AV1 configuration, pixel aspect ratio, and several image-item properties/relationships are missing or parsed without fully affecting output. +- Pixel aspect ratio now preserves the complete unsigned spacing pair, affects Decode and Identify through ImageSharp's existing unitless resolution metadata, and follows quarter-turn presentation rotation. Independent grid, rotation, and maximum-spacing fixtures remain required. +- HEVC and AV1 configuration and several image-item properties/relationships are missing or parsed without fully 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. ### HEVC decoder and encoder diff --git a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs index 3adbc100a..1084f643e 100644 --- a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs +++ b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs @@ -243,6 +243,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore if (!this.Options.SkipMetadata) { this.ApplyItemColorMetadata(metadata, presentationItem); + this.ApplyItemPixelAspectRatioMetadata(metadata, presentationItem); } } @@ -801,9 +802,18 @@ internal sealed class HeifDecoderCore : ImageDecoderCore break; case Heif4CharCode.Pasp: EnsureBufferRemaining(boxBuffer, 0, 8, "pixel aspect ratio"); - int horizontalSpacing = (int)BinaryPrimitives.ReadUInt32BigEndian(boxBuffer); - int verticalSpacing = (int)BinaryPrimitives.ReadUInt32BigEndian(boxBuffer[4..]); - properties.Add(new KeyValuePair(Heif4CharCode.Pasp, new Size(horizontalSpacing, verticalSpacing))); + uint horizontalSpacing = BinaryPrimitives.ReadUInt32BigEndian(boxBuffer); + uint verticalSpacing = BinaryPrimitives.ReadUInt32BigEndian(boxBuffer[4..]); + if (horizontalSpacing == 0 || verticalSpacing == 0) + { + throw new InvalidImageContentException("The pixel aspect ratio property has zero spacing."); + } + + properties.Add( + new KeyValuePair( + Heif4CharCode.Pasp, + new HeifPixelAspectRatio(horizontalSpacing, verticalSpacing))); + break; case Heif4CharCode.Pixi: EnsureBufferRemaining(boxBuffer, 0, 5, "pixel information"); @@ -1035,7 +1045,12 @@ internal sealed class HeifDecoderCore : ImageDecoderCore item.SetExtent((Size)prop.Value); break; case Heif4CharCode.Pasp: - item.PixelAspectRatio = (Size)prop.Value; + if (item.PixelAspectRatio is not null) + { + throw new InvalidImageContentException($"Item {itemId} associates more than one pixel aspect ratio property."); + } + + item.PixelAspectRatio = (HeifPixelAspectRatio)prop.Value; break; case Heif4CharCode.Pixi: int[] values = (int[])prop.Value; @@ -1403,6 +1418,11 @@ internal sealed class HeifDecoderCore : ImageDecoderCore // implemented transforms after alpha composition keeps the auxiliary plane in the same coordinate space. ApplyPresentationTransforms(image, itemToDecode); + if (!this.Options.SkipMetadata) + { + this.ApplyItemPixelAspectRatioMetadata(image.Metadata, itemToDecode); + } + // 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. HeifMetadata meta = image.Metadata.GetHeifMetadata(); @@ -1444,6 +1464,37 @@ internal sealed class HeifDecoderCore : ImageDecoderCore } } + /// + /// Applies the pixel aspect ratio associated with a presented still-image item. + /// + /// The image metadata receiving the aspect ratio. + /// The image item whose pixels are presented. + private void ApplyItemPixelAspectRatioMetadata(ImageMetadata metadata, HeifItem imageItem) + { + HeifItem? gridTile = imageItem.Type == Heif4CharCode.Grid + ? this.FindDecodableGridTile(imageItem) + : null; + + HeifPixelAspectRatio? pixelAspectRatio = imageItem.PixelAspectRatio ?? gridTile?.PixelAspectRatio; + if (pixelAspectRatio is null) + { + return; + } + + // ImageMetadata expresses pixel width:height as vertical-density:horizontal-density. A quarter-turn exchanges + // the displayed pixel axes, so it also exchanges which spacing value supplies each density. + bool swapsAxes = imageItem.RotationAngle is 1 or 3; + metadata.HorizontalResolution = swapsAxes + ? pixelAspectRatio.HorizontalSpacing + : pixelAspectRatio.VerticalSpacing; + + metadata.VerticalResolution = swapsAxes + ? pixelAspectRatio.VerticalSpacing + : pixelAspectRatio.HorizontalSpacing; + + metadata.ResolutionUnits = PixelResolutionUnit.AspectRatio; + } + /// /// Applies Exif and XMP metadata items that describe a decoded color image item. /// diff --git a/src/ImageSharp/Formats/Heif/HeifItem.cs b/src/ImageSharp/Formats/Heif/HeifItem.cs index 75327c1de..5d374d90f 100644 --- a/src/ImageSharp/Formats/Heif/HeifItem.cs +++ b/src/ImageSharp/Formats/Heif/HeifItem.cs @@ -66,9 +66,10 @@ internal class HeifItem(Heif4CharCode type, uint id) public CicpProfile? CicpProfile { get; set; } /// - /// Gets or sets the aspect ratio of the pixels. + /// Gets or sets the relative pixel spacing associated with this image item, or when the + /// item has no pixel-aspect-ratio property. /// - public Size PixelAspectRatio { get; set; } + public HeifPixelAspectRatio? PixelAspectRatio { get; set; } /// /// Gets or sets the clean-aperture crop applied before image rotation and mirroring, or diff --git a/src/ImageSharp/Formats/Heif/HeifPixelAspectRatio.cs b/src/ImageSharp/Formats/Heif/HeifPixelAspectRatio.cs new file mode 100644 index 000000000..e72b0d3ed --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifPixelAspectRatio.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Describes the relative horizontal and vertical spacing of pixels in a HEIF still-image item. +/// +/// The relative horizontal pixel spacing. +/// The relative vertical pixel spacing. +internal sealed class HeifPixelAspectRatio(uint horizontalSpacing, uint verticalSpacing) +{ + /// + /// Gets the relative horizontal pixel spacing. + /// + public uint HorizontalSpacing { get; } = horizontalSpacing; + + /// + /// Gets the relative vertical pixel spacing. + /// + public uint VerticalSpacing { get; } = verticalSpacing; +}