diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index ca2866c8a6..7157dba3bf 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -37,6 +37,8 @@ Out of scope are movie and media boxes, tracks, sample tables, timing and edit m Implementation rule: do not introduce a reusable general-purpose ISO BMFF box hierarchy, track model, or media parser. Add box syntax directly to the bounded HEIF container model only when a supported still-image item, relationship, property, metadata path, or conformance fixture requires it. Each addition must name the image behavior it enables and have a focused image-format test. +Codec-configuration rule: parse `av1C` and `hvcC` only as properties of coded still-image items. Validate their image profile, level, bit depth, chroma layout, and parameter-set/OBU declarations against the associated item payload and expose only image metadata needed by ImageSharp. Do not port visual sample entries, sample descriptions, decoder-configuration records for tracks, layer-selection state, sample groups, timing, or any other movie-oriented ISO BMFF surface around those records. + ## Reference hierarchy Use the references in this order when behavior differs: @@ -92,6 +94,7 @@ This snapshot pins or classifies the available references and failures; it does | `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. | +| `Av1CodecConfiguration`, `HeifItem.Av1CodecConfiguration`, `Av1HeifItemDecoder`, and AV1 grid configuration checks | AV1-ISOBMFF sections 2.3.3 and 2.3.4 codec-configuration record syntax and semantics; AVIF section 2.2.1 AV1 item-configuration property; MIAF section 7.3.11.4.1 grid input constraints | libavif `src/read.c` functions `avifParseCodecConfiguration`, `avifDecoderItemValidateProperties`, and `avifReadCodecConfigProperty` at `092276ce89098ead06db80975173191e5fee1826` | Associate exactly one `av1C` property with each decoded `av01` image item, validate the fixed record and its bit depth/chroma fields against the item's AV1 sequence header and optional `pixi` channel depths, require matching configurations across grid tiles, and report the encoded image precision and monochrome shape through `HeifMetadata`. Preserve optional configuration OBUs for the remaining item-level semantic validation. Consume but do not retain presentation-delay syntax, and introduce no sample entry, sample description, track, timing, or generic decoder-configuration 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. | @@ -123,7 +126,8 @@ This assessment is based on the current source after the upstream ImageSharp mer - 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. - 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. +- AV1 codec configuration is now retained per `av01` image item rather than in decoder-global state. Decode requires the property, validates its fixed record against the item's sequence header and any associated `pixi` channel depths, and requires matching configurations across grid tiles. Identify now reports the configuration's 8/10/12-bit precision and monochrome shape. Optional configuration OBUs are preserved but their duplicate sequence-header and metadata semantics still require validation; HEVC `hvcC` remains unimplemented. +- Several image-item properties and relationships remain 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/Av1/Av1CodecConfiguration.cs b/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs index 3928ad67ad..3a28f0b923 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs @@ -1,105 +1,166 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + namespace SixLabors.ImageSharp.Formats.Heif.Av1; /// -/// Represents the decoder configuration fields stored in an AV1 codec-configuration property. +/// Contains the image-description fields stored in an AV1 codec-configuration item property. /// -internal struct Av1CodecConfiguration +internal sealed class Av1CodecConfiguration { /// - /// Initializes a new instance of the struct from an AV1 codec-configuration payload. + /// The optional open bitstream units following the fixed four-byte configuration record. + /// + private readonly byte[] configObus; + + /// + /// Initializes a new instance of the class from an AV1 codec-configuration + /// item-property payload. /// /// The configuration payload beginning with the marker and version fields. public Av1CodecConfiguration(Span boxBuffer) { + if (boxBuffer.Length < 4) + { + throw new InvalidImageContentException("The AV1 codec configuration is truncated."); + } + Av1BitStreamReader reader = new(boxBuffer); + uint marker = reader.ReadLiteral(1); + uint version = reader.ReadLiteral(7); + if (marker != 1 || version != 1) + { + throw new InvalidImageContentException("The AV1 codec configuration has an invalid marker or version."); + } - this.Marker = (byte)reader.ReadLiteral(1); - this.Version = (byte)reader.ReadLiteral(7); - this.SeqProfile = (byte)reader.ReadLiteral(3); - this.SeqLevelIdx0 = (byte)reader.ReadLiteral(5); - this.SeqTier0 = (byte)reader.ReadLiteral(1); - this.HighBitdepth = (byte)reader.ReadLiteral(1); + this.SequenceProfile = (byte)reader.ReadLiteral(3); + this.SequenceLevelIndex = (byte)reader.ReadLiteral(5); + this.SequenceTier = reader.ReadLiteral(1) == 1; + this.HighBitDepth = reader.ReadLiteral(1) == 1; this.TwelveBit = reader.ReadLiteral(1) == 1; - this.MonoChrome = reader.ReadLiteral(1) == 1; + this.IsMonochrome = reader.ReadLiteral(1) == 1; this.ChromaSubsamplingX = reader.ReadLiteral(1) == 1; this.ChromaSubsamplingY = reader.ReadLiteral(1) == 1; this.ChromaSamplePosition = (byte)reader.ReadLiteral(2); + if (this.SequenceProfile > (byte)ObuSequenceProfile.Professional + || (this.TwelveBit && !this.HighBitDepth) + || this.ChromaSamplePosition == (byte)ObuChromoSamplePosition.Reserved) + { + throw new InvalidImageContentException("The AV1 codec configuration contains invalid image-description fields."); + } - // 3 bits are reserved. - reader.ReadLiteral(3); + if (reader.ReadLiteral(3) != 0) + { + throw new InvalidImageContentException("The AV1 codec configuration has nonzero reserved bits."); + } - this.InitialPresentationDelayPresent = reader.ReadLiteral(1) == 1; - if (this.InitialPresentationDelayPresent) + bool hasInitialPresentationDelay = reader.ReadLiteral(1) == 1; + uint delayOrReserved = reader.ReadLiteral(4); + if (!hasInitialPresentationDelay && delayOrReserved != 0) { - byte initialPresentationDelayMinusOne = (byte)reader.ReadLiteral(4); - this.InitialPresentationDelay = (byte)(initialPresentationDelayMinusOne + 1); + throw new InvalidImageContentException("The AV1 codec configuration has a nonzero reserved delay field."); } - } - /// - /// Gets the one-bit configuration marker. - /// - public byte Marker { get; } + // The delay syntax is consumed to validate the fixed record, but it describes sample presentation and has + // no meaning for the independently presented image item supported by this bounded container implementation. + this.configObus = boxBuffer[4..].ToArray(); + } /// - /// Gets the codec-configuration record version. + /// Gets the sequence profile declared for the coded image. /// - public byte Version { get; } + public byte SequenceProfile { get; } /// - /// Gets the sequence profile declared by the configuration record. + /// Gets the first operating point's sequence-level index. /// - public byte SeqProfile { get; } + public byte SequenceLevelIndex { get; } /// - /// Gets the first operating point's sequence level index. + /// Gets a value indicating whether the first operating point uses the high tier. /// - public byte SeqLevelIdx0 { get; } + public bool SequenceTier { get; } /// - /// Gets the first operating point's sequence tier flag. + /// Gets a value indicating whether the coded image uses more than eight bits per sample. /// - public byte SeqTier0 { get; } + public bool HighBitDepth { get; } /// - /// Gets the high-bit-depth flag. + /// Gets a value indicating whether the coded image uses twelve bits per sample. /// - public byte HighBitdepth { get; } + public bool TwelveBit { get; } /// - /// Gets a value indicating whether the sequence uses twelve-bit samples. + /// Gets the coded image sample precision in bits. /// - public bool TwelveBit { get; } + public int BitDepth => this.TwelveBit ? 12 : this.HighBitDepth ? 10 : 8; /// - /// Gets a value indicating whether the sequence contains only a luma plane. + /// Gets a value indicating whether the coded image contains only a luma plane. /// - public bool MonoChrome { get; } + public bool IsMonochrome { get; } /// - /// Gets a value indicating whether chroma is horizontally subsampled. + /// Gets a value indicating whether the coded image's chroma planes are horizontally subsampled. /// public bool ChromaSubsamplingX { get; } /// - /// Gets a value indicating whether chroma is vertically subsampled. + /// Gets a value indicating whether the coded image's chroma planes are vertically subsampled. /// public bool ChromaSubsamplingY { get; } /// - /// Gets the chroma sample-position code. + /// Gets the position of vertically subsampled chroma samples relative to luma samples. /// public byte ChromaSamplePosition { get; } /// - /// Gets a value indicating whether an initial presentation delay is declared. + /// Gets the optional configuration open bitstream units following the fixed record. /// - public bool InitialPresentationDelayPresent { get; } + public ReadOnlyMemory ConfigObus => this.configObus; /// - /// Gets the initial presentation delay in decoded frames, or zero when no delay is declared. + /// Determines whether another item configuration describes the same coded-image sample layout. /// - public byte InitialPresentationDelay { get; } + /// The configuration to compare. + /// when every fixed image-description field is equal. + public bool HasMatchingImageConfiguration(Av1CodecConfiguration other) + => this.SequenceProfile == other.SequenceProfile + && this.SequenceLevelIndex == other.SequenceLevelIndex + && this.SequenceTier == other.SequenceTier + && this.HighBitDepth == other.HighBitDepth + && this.TwelveBit == other.TwelveBit + && this.IsMonochrome == other.IsMonochrome + && this.ChromaSubsamplingX == other.ChromaSubsamplingX + && this.ChromaSubsamplingY == other.ChromaSubsamplingY + && this.ChromaSamplePosition == other.ChromaSamplePosition; + + /// + /// Validates the configuration fields against the sequence header that describes the coded image item. + /// + /// The decoded AV1 sequence header. + public void Validate(ObuSequenceHeader sequenceHeader) + { + ObuOperatingPoint operatingPoint = sequenceHeader.OperatingPoint[0]; + ObuColorConfig colorConfig = sequenceHeader.ColorConfig; + bool highBitDepth = colorConfig.BitDepth is Av1BitDepth.TenBit or Av1BitDepth.TwelveBit; + bool twelveBit = colorConfig.BitDepth == Av1BitDepth.TwelveBit; + + if (this.SequenceProfile != (byte)sequenceHeader.SequenceProfile + || this.SequenceLevelIndex != operatingPoint.SequenceLevelIndex + || this.SequenceTier != (operatingPoint.SequenceTier != 0) + || this.HighBitDepth != highBitDepth + || this.TwelveBit != twelveBit + || this.IsMonochrome != colorConfig.IsMonochrome + || this.ChromaSubsamplingX != colorConfig.SubSamplingX + || this.ChromaSubsamplingY != colorConfig.SubSamplingY + || this.ChromaSamplePosition != (byte)colorConfig.ChromaSamplePosition) + { + throw new InvalidImageContentException("The AV1 item configuration does not match its sequence header."); + } + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs b/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs index cfe06249c3..efd14b3dfb 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs @@ -63,8 +63,14 @@ internal class Av1Decoder : IAv1TileReader /// /// The container color description that overrides matching sequence-header color information. /// + /// + /// The item-associated AV1 codec configuration validated against the coded sequence header. + /// /// The decoded image. - public Image Decode(Span buffer, CicpProfile? containerColorProfile = null) + public Image Decode( + Span buffer, + CicpProfile? containerColorProfile = null, + Av1CodecConfiguration? codecConfiguration = null) where TPixel : unmanaged, IPixel { Av1BitStreamReader reader = new(buffer); @@ -72,6 +78,7 @@ internal class Av1Decoder : IAv1TileReader Guard.NotNull(this.tileReader, nameof(this.tileReader)); Guard.NotNull(this.SequenceHeader, nameof(this.SequenceHeader)); Guard.NotNull(this.FrameHeader, nameof(this.FrameHeader)); + codecConfiguration?.Validate(this.SequenceHeader); if (containerColorProfile is not null) { diff --git a/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs index 6a7d1e1a8a..186c35f6a6 100644 --- a/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs @@ -40,7 +40,26 @@ internal class Av1HeifItemDecoder : IHeifItemDecoder Span data, CicpProfile? colorProfile) { + Av1CodecConfiguration codecConfiguration = item.Av1CodecConfiguration + ?? throw new InvalidImageContentException($"AV1 image item {item.Id} has no codec configuration property."); + + if (item.ChannelBitDepths is not null) + { + foreach (byte channelBitDepth in item.ChannelBitDepths) + { + if (channelBitDepth != codecConfiguration.BitDepth) + { + throw new InvalidImageContentException($"AV1 image item {item.Id} has mismatched pixel-information and codec-configuration bit depths."); + } + } + } + Av1Decoder decoder = new(configuration); - return decoder.Decode(data, colorProfile); + Image image = decoder.Decode(data, colorProfile, codecConfiguration); + HeifMetadata metadata = image.Metadata.GetHeifMetadata(); + metadata.CompressionMethod = this.CompressionMethod; + metadata.BitDepth = codecConfiguration.BitDepth; + metadata.IsMonochrome = codecConfiguration.IsMonochrome; + return image; } } diff --git a/src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs index 28f917559a..4d74420f45 100644 --- a/src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs +++ b/src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Buffers.Binary; using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.PixelFormats; @@ -153,6 +154,7 @@ internal class GridHeifItemDecoder : IHeifItemDecoder // the final grid has copied its pixels, then dispose all intermediates together. using DisposableList> gridTiles = new(linked.Count); Heif4CharCode tileType = default; + Av1CodecConfiguration? av1GridConfiguration = null; foreach (uint id in linked) { HeifItem item = this.items.First(item => item.Id == id); @@ -165,6 +167,23 @@ internal class GridHeifItemDecoder : IHeifItemDecoder throw new InvalidImageContentException("All HEIF image grid tiles must use the same coding format."); } + if (item.Type == Heif4CharCode.Av01) + { + Av1CodecConfiguration itemConfiguration = item.Av1CodecConfiguration + ?? throw new InvalidImageContentException($"AV1 image grid tile {item.Id} has no codec configuration property."); + + if (av1GridConfiguration is null) + { + av1GridConfiguration = itemConfiguration; + } + else if (!av1GridConfiguration.HasMatchingImageConfiguration(itemConfiguration)) + { + // All grid cells share one output sample layout. Reject differing AV1 descriptions before + // allocating and copying tiles so channel precision or chroma geometry cannot change by cell. + throw new InvalidImageContentException("All AV1 image grid tiles must use matching codec configurations."); + } + } + IHeifItemDecoder? decoder = HeifCompressionFactory.GetDecoder(item.Type); if (decoder is null) { diff --git a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs index 1084f643ed..521dd8d103 100644 --- a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs +++ b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs @@ -67,11 +67,6 @@ internal sealed class HeifDecoderCore : ImageDecoderCore /// private readonly List itemLinks; - /// - /// The codec configuration associated with the current AV1 item. - /// - private Av1CodecConfiguration av1CodecConfiguration; - /// /// The absolute stream offset of the item-data box payload, or -1 when no item-data box exists. /// @@ -217,6 +212,43 @@ internal sealed class HeifDecoderCore : ImageDecoderCore HeifItem? thumbnail = gridTile is null ? this.FindDecodableThumbnail(item) : null; metadataItem = gridTile ?? thumbnail ?? item; presentationItem = thumbnail ?? item; + if (gridTile is not null) + { + Av1CodecConfiguration? gridConfiguration = gridTile.Type == Heif4CharCode.Av01 + ? gridTile.Av1CodecConfiguration + ?? throw new InvalidImageContentException($"AV1 image grid tile {gridTile.Id} has no codec configuration property.") + : null; + + foreach (HeifItemLink link in this.itemLinks) + { + if (link.Type != Heif4CharCode.Dimg || link.SourceId != item.Id) + { + continue; + } + + foreach (uint tileId in link.DestinationIds) + { + HeifItem tile = this.FindItemById(tileId)!; + if (tile.Type != gridTile.Type) + { + throw new InvalidImageContentException("All HEIF image grid tiles must use the same coding format."); + } + + if (gridConfiguration is not null) + { + Av1CodecConfiguration tileConfiguration = tile.Av1CodecConfiguration + ?? throw new InvalidImageContentException($"AV1 image grid tile {tile.Id} has no codec configuration property."); + + // Identify never reads the derived-image descriptor or coded tile payloads, but it still + // validates the shared sample layout needed to describe the displayed grid accurately. + if (!gridConfiguration.HasMatchingImageConfiguration(tileConfiguration)) + { + throw new InvalidImageContentException("All AV1 image grid tiles must use matching codec configurations."); + } + } + } + } + } } else if (HeifCompressionFactory.GetDecoder(item.Type) is null) { @@ -229,7 +261,12 @@ internal sealed class HeifDecoderCore : ImageDecoderCore HeifCompressionMethod compressionMethod = HeifCompressionMethod.Hevc; if (metadataItem.Type == Heif4CharCode.Av01) { + Av1CodecConfiguration codecConfiguration = metadataItem.Av1CodecConfiguration + ?? throw new InvalidImageContentException($"AV1 image item {metadataItem.Id} has no codec configuration property."); + compressionMethod = HeifCompressionMethod.Av1; + meta.BitDepth = codecConfiguration.BitDepth; + meta.IsMonochrome = codecConfiguration.IsMonochrome; } else if (metadataItem.Type == Heif4CharCode.Jpeg) { @@ -817,18 +854,35 @@ internal sealed class HeifDecoderCore : ImageDecoderCore break; case Heif4CharCode.Pixi: EnsureBufferRemaining(boxBuffer, 0, 5, "pixel information"); + if (boxBuffer[0] != 0 || boxBuffer[1] != 0 || boxBuffer[2] != 0 || boxBuffer[3] != 0) + { + throw new InvalidImageContentException("The pixel information property has an unsupported version or flags."); + } // The full-box header precedes one bit-depth byte for each channel. int channelCount = boxBuffer[4]; + if (channelCount == 0) + { + throw new InvalidImageContentException("The pixel information property has no channels."); + } + int offset = 5; EnsureBufferRemaining(boxBuffer, offset, channelCount, "pixel information"); - int bitsPerPixel = 0; - for (int i = 0; i < channelCount; i++) + if (boxBuffer.Length != offset + channelCount) + { + throw new InvalidImageContentException("The pixel information property contains unexpected trailing data."); + } + + byte[] channelBitDepths = boxBuffer.Slice(offset, channelCount).ToArray(); + for (int i = 0; i < channelBitDepths.Length; i++) { - bitsPerPixel += boxBuffer[offset + i]; + if (channelBitDepths[i] == 0) + { + throw new InvalidImageContentException($"The pixel information property declares zero precision for channel {i}."); + } } - properties.Add(new KeyValuePair(Heif4CharCode.Pixi, new int[] { channelCount, bitsPerPixel })); + properties.Add(new KeyValuePair(Heif4CharCode.Pixi, channelBitDepths)); break; case Heif4CharCode.AuxC: @@ -905,8 +959,11 @@ internal sealed class HeifDecoderCore : ImageDecoderCore break; case Heif4CharCode.Av1C: EnsureBufferRemaining(boxBuffer, 0, 4, "AV1 codec configuration"); - this.av1CodecConfiguration = new(boxBuffer); - properties.Add(new KeyValuePair(Heif4CharCode.Av1C, new object())); + properties.Add( + new KeyValuePair( + Heif4CharCode.Av1C, + new Av1CodecConfiguration(boxBuffer))); + break; case Heif4CharCode.Clap: EnsureBufferRemaining(boxBuffer, 0, 32, "clean aperture"); @@ -1053,9 +1110,34 @@ internal sealed class HeifDecoderCore : ImageDecoderCore item.PixelAspectRatio = (HeifPixelAspectRatio)prop.Value; break; case Heif4CharCode.Pixi: - int[] values = (int[])prop.Value; - item.ChannelCount = values[0]; - item.BitsPerPixel = values[1]; + if (item.ChannelBitDepths is not null) + { + throw new InvalidImageContentException($"Item {itemId} associates more than one pixel information property."); + } + + byte[] channelBitDepths = (byte[])prop.Value; + int bitsPerPixel = 0; + for (int channel = 0; channel < channelBitDepths.Length; channel++) + { + bitsPerPixel += channelBitDepths[channel]; + } + + item.ChannelCount = channelBitDepths.Length; + item.ChannelBitDepths = channelBitDepths; + item.BitsPerPixel = bitsPerPixel; + break; + case Heif4CharCode.Av1C: + if (item.Type != Heif4CharCode.Av01) + { + throw new InvalidImageContentException($"Item {itemId} associates an AV1 codec configuration with non-AV1 item type '{PrettyPrint(item.Type)}'."); + } + + if (item.Av1CodecConfiguration is not null) + { + throw new InvalidImageContentException($"Item {itemId} associates more than one AV1 codec configuration property."); + } + + item.Av1CodecConfiguration = (Av1CodecConfiguration)prop.Value; break; case Heif4CharCode.AuxC: if (item.AuxiliaryType is not null) diff --git a/src/ImageSharp/Formats/Heif/HeifItem.cs b/src/ImageSharp/Formats/Heif/HeifItem.cs index 5d374d90f3..c2f03223ad 100644 --- a/src/ImageSharp/Formats/Heif/HeifItem.cs +++ b/src/ImageSharp/Formats/Heif/HeifItem.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.Metadata.Profiles.Icc; @@ -65,6 +66,12 @@ internal class HeifItem(Heif4CharCode type, uint id) /// public CicpProfile? CicpProfile { get; set; } + /// + /// Gets or sets the AV1 codec configuration associated with this coded image item, or + /// when the item has no AV1 codec-configuration property. + /// + public Av1CodecConfiguration? Av1CodecConfiguration { get; set; } + /// /// Gets or sets the relative pixel spacing associated with this image item, or when the /// item has no pixel-aspect-ratio property. @@ -94,6 +101,12 @@ internal class HeifItem(Heif4CharCode type, uint id) /// public int ChannelCount { get; set; } + /// + /// Gets or sets the encoded precision of each image channel, or when the item has no + /// pixel-information property. + /// + public byte[]? ChannelBitDepths { get; set; } + /// /// Gets or sets the number of bits in a single pixel. /// diff --git a/src/ImageSharp/Formats/Heif/HeifMetadata.cs b/src/ImageSharp/Formats/Heif/HeifMetadata.cs index dbab577138..e71797c326 100644 --- a/src/ImageSharp/Formats/Heif/HeifMetadata.cs +++ b/src/ImageSharp/Formats/Heif/HeifMetadata.cs @@ -25,6 +25,8 @@ public class HeifMetadata : IFormatMetadata private HeifMetadata(HeifMetadata other) { this.CompressionMethod = other.CompressionMethod; + this.BitDepth = other.BitDepth; + this.IsMonochrome = other.IsMonochrome; this.HasAlpha = other.HasAlpha; } @@ -33,6 +35,16 @@ public class HeifMetadata : IFormatMetadata /// public HeifCompressionMethod CompressionMethod { get; set; } + /// + /// Gets or sets the encoded precision of each color component in bits. + /// + public int BitDepth { get; set; } = 8; + + /// + /// Gets or sets a value indicating whether the primary image contains a single luminance component. + /// + public bool IsMonochrome { get; set; } + /// /// Gets or sets a value indicating whether the primary image has an alpha channel. /// @@ -42,19 +54,39 @@ public class HeifMetadata : IFormatMetadata public static HeifMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => new() { CompressionMethod = HeifCompressionMethod.LegacyJpeg, + BitDepth = metadata.PixelTypeInfo.ComponentInfo?.GetMaximumComponentPrecision() ?? 8, + IsMonochrome = metadata.PixelTypeInfo.ColorType.HasFlag(PixelColorType.Luminance) + && !metadata.PixelTypeInfo.ColorType.HasFlag(PixelColorType.ChrominanceBlue), HasAlpha = metadata.PixelTypeInfo.AlphaRepresentation != PixelAlphaRepresentation.None }; /// public PixelTypeInfo GetPixelTypeInfo() { - int bpp = this.HasAlpha ? 32 : 24; - PixelColorType colorType = this.HasAlpha ? PixelColorType.RGB | PixelColorType.Alpha : PixelColorType.RGB; - PixelComponentInfo info = this.HasAlpha - ? PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8) - : PixelComponentInfo.Create(3, bpp, 8, 8, 8); + int colorComponentCount = this.IsMonochrome ? 1 : 3; + int componentCount = colorComponentCount + (this.HasAlpha ? 1 : 0); + int bitsPerPixel = componentCount * this.BitDepth; + PixelColorType colorType = this.IsMonochrome ? PixelColorType.Luminance : PixelColorType.RGB; + PixelComponentInfo info; + if (this.IsMonochrome) + { + info = this.HasAlpha + ? PixelComponentInfo.Create(2, bitsPerPixel, this.BitDepth, this.BitDepth) + : PixelComponentInfo.Create(1, bitsPerPixel, this.BitDepth); + } + else + { + info = this.HasAlpha + ? PixelComponentInfo.Create(4, bitsPerPixel, this.BitDepth, this.BitDepth, this.BitDepth, this.BitDepth) + : PixelComponentInfo.Create(3, bitsPerPixel, this.BitDepth, this.BitDepth, this.BitDepth); + } + + if (this.HasAlpha) + { + colorType |= PixelColorType.Alpha; + } - return new PixelTypeInfo(bpp) + return new PixelTypeInfo(bitsPerPixel) { AlphaRepresentation = this.HasAlpha ? PixelAlphaRepresentation.Unassociated : PixelAlphaRepresentation.None, ColorType = colorType,