diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 17f0fc602..1ee8f0d4c 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -46,8 +46,16 @@ Checkboxes may be marked complete only when the implementation and the verificat - [x] Reconcile the top-level still-image-only scope with the required animated HEIC and AVIF completion boundary before sequence implementation begins. - [x] Define the ImageSharp image-level sequence metadata and per-frame metadata contracts, including observable timing, repetition, color, alpha, orientation, and profile behavior. - `HeifMetadata` now carries repetition and root-frame animation behavior through `FormatConnectingMetadata`. `HeifFrameMetadata` carries frame duration through `FormatConnectingFrameMetadata`; frame-local color, alpha, orientation, and profile state remain owned by the existing `ImageFrameMetadata` contract. The focused Release suite passes all 40 encoder, image metadata, and frame metadata tests. - - [ ] Identify and document the minimum normative ISO BMFF track, sample-description, sample-location, dependency, and timing syntax required by conforming HEIC and AVIF image sequences. + - [x] Identify and document the minimum normative ISO BMFF track, sample-description, sample-location, dependency, and timing syntax required by conforming HEIC and AVIF image sequences. + - The bounded syntax matrix below is derived from ISO/IEC 23008-12:2022 clauses 7 through 10, the codec-specific HEVC Annex B, AVIF 1.2 section 3, AV1-ISOBMFF 1.2 section 2, and the pinned libavif `read.c` and `write.c` sequence paths. - [ ] Parse and write only that bounded image-sequence syntax; do not add audio, arbitrary media, editing, fragmentation, streaming, or general presentation APIs. + - [x] Share one bounded 32/64-bit box-header reader between the existing item path and the image-sequence path, including short-read handling and parent-boundary validation. + - [x] Select one enabled `pict` master track without materializing unrelated tracks, then parse its `mvhd`/`tkhd`/`mdhd`/`hdlr`, self-contained `dref`, `av01` or `hvc1` sample entry, codec configuration, mandatory `ccst`, and bounded repetition edit. + - [x] Resolve `stsc`, `stco`/`co64`, `stsz`/`stz2`, `stts`, and `stss` into one exact value-type descriptor array capped by `DecoderOptions.MaxFrames`; validate complete run/count syntax through one allocator-owned sequential scratch buffer. + - Release verification passes the libavif-shaped two-sample parser fixture, the one-frame retention boundary, and a sample offset/length beyond the file. The parser performs no per-entry allocation and does not buffer `moov`, `mdat`, or complete attacker-sized tables. + - [ ] Parse composition offsets and hidden samples from `ctts`/`cslg`, optional direct dependencies from `refs` sample groups, track presentation/color/HDR properties, and bounded sequence metadata items. + - [ ] Connect the parsed sequence index to HEIF detection, Identify, frame decode, alpha matching, and frame metadata without changing still-image source selection. + - [ ] Write the same bounded movie, track, sample-description, location, dependency, timing, repetition, alpha, and metadata syntax from ImageSharp frames. - [ ] Decode frame dependencies, durations, repetition, frame-local auxiliary images, and frame-local metadata into the existing ImageSharp multi-frame model. - [ ] Encode ImageSharp frames, durations, repetition, frame-local auxiliary images, and frame-local metadata as independently decodable HEIC and AVIF image sequences. - [ ] Add malformed-sequence boundary coverage and independently verify animated inputs and outputs with pinned HEIC and AVIF implementations. @@ -96,6 +104,27 @@ Codec-configuration rule: parse `av1C` and `hvcC` as properties of coded image i AV1 sequence headers and HEVC VPS/SPS/PPS structures remain in scope because they are codec syntax required by image items and image-sequence frames. Retained reference-frame state is permitted only for the lifetime of a bounded HEIC/AVIF sequence decode or encode operation. +### Bounded image-sequence syntax + +The sequence reader and writer may retain only the following syntax and the resolved image behavior listed beside it: + +| Syntax | Required image behavior | +| --- | --- | +| `ftyp` sequence and structural brands | Recognize `avis` AV1 sequences and the non-layered `hevc`/`hevx` HEVC sequence profiles. `avio` can additionally signal an all-sync AV1 sequence. Layered `hevm`/`hevs`, JPEG `jpgs`, arbitrary video brands, and brands for unimplemented codecs remain unsupported until their image payload and presentation requirements are implemented. | +| `moov`/`mvhd`, `trak`/`tkhd`, and `mdia`/`mdhd`/`hdlr` | Select an enabled, in-movie `pict` master image-sequence track; retain its displayed dimensions, presentation matrix, media time scale, media duration, and movie-time-scale track duration. Ignore unrelated tracks rather than exposing them. | +| `minf`/`dinf`/`dref` and `stbl` | Accept only self-contained sample data references and own the bounded sample-table state for one selected image sequence plus an optional linked auxiliary-alpha sequence. No reusable data-reference, media-information, or sample-table API is created. | +| `stsd` and one supported visual sample entry | Require exactly one `av01` entry for AVIF or one non-layered `hvc1` entry for HEIC. Retain only its dimensions, codec configuration (`av1C` or `hvcC`), image presentation/color/HDR properties, and mandatory version-zero `ccst` coding constraints. Reject an unsupported essential configuration rather than treating it as generic video. | +| `stsc`, `stco`/`co64`, and `stsz`/`stz2` | Resolve each declared image sample directly to a validated file offset and length. Expand run tables once into a compact frame-owned descriptor array bounded by `DecoderOptions.MaxFrames`; never buffer the movie or complete `mdat`. | +| `stts` | Expand decode-order sample durations against the media time scale into exact per-frame rational delays. A sequence with advisory timing still maps every visible sample to an ImageSharp frame. | +| optional `ctts` version 0/1 and `cslg` | Retain only composition offsets and the HEIF hidden-sample condition required to determine presented frame order and visibility. No general composition timeline is exposed. | +| optional `stss` | Mark the one-based random-access samples; absence means every sample is a sync sample. The first retained frame must have a valid decode path from a sync sample. | +| optional `sgpd`/`sbgp` with grouping type `refs` | Resolve direct-reference sample identifiers for bounded dependency validation and random access. Other sample-group types are ignored unless a later image-format requirement explicitly brings them into scope. | +| optional `edts`/`elst` | Interpret only normal-rate image-sequence presentation and the HEIF repetition flag. Reject edits that require arbitrary media splicing, rate changes, or a general edit timeline. | +| optional `tref` entries `auxl`, `prem`, `thmb`, and `cdsc` | Link only image alpha, premultiplication, image thumbnails, and image metadata. Auxiliary samples are matched to color frames by their decoded time; audio and every unrelated reference type are ignored. | +| optional track-level `meta` and image metadata linkage | Apply sequence-wide Exif/XMP profiles from the bounded track metadata model. Per-frame metadata is limited to supported metadata items/groups or codec metadata that can be mapped to `ImageFrameMetadata`; generic timed-metadata tracks are not exposed. | + +The implementation must validate uniqueness, versions, entry counts, run totals, one-based indices, integer products, cumulative durations, offsets, and lengths before creating retained arrays. Temporary box payloads use the configured `MemoryAllocator`; persistent sample descriptors are allocated once at the validated retained-frame count. Parsing must skip unrelated boxes by their bounded length and must not copy an entire `moov` or `mdat` payload. + ## Reference hierarchy Use the references in this order when behavior differs: @@ -148,7 +177,8 @@ This snapshot pins or classifies the available references and failures; it does | `Av1ForwardTransformer`, `Av1Inverse2dTransformer`, `Av1Transform2dFlipConfiguration`, the forward/inverse 1-D operator structs, and `Av1Transform1dMath` | AV1 forward transform definitions and section 7.11.2 inverse transform and reconstruction | libaom `av1/encoder/av1_fwd_txfm1d.c`, `av1/encoder/av1_fwd_txfm2d.c`, `av1/common/av1_inv_txfm1d_cfg.h`, `av1/common/av1_inv_txfm1d.c`, `av1/common/av1_inv_txfm2d.c`, the x86 AVX2/SSE4 implementations, and the corresponding Neon implementations at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve the normative staged DCT, ADST, and identity arithmetic, direction-specific stage ranges and shifts, transposition, clipping, and high-bit-depth sample addition. Stateless static-generic operators follow ImageSharp's JPEG color-transform pattern. `Vector128` and `Vector256` kernels retain wrapping 32-bit lane arithmetic before the bounded rounding shift, matching the optimized upstream implementations, with the scalar operators as the behavioral oracle. The pinned upstream transform paths do not provide an AVX-512 implementation. | | `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. | | `HeifCleanAperture`, `HeifItem` presentation state, and `HeifDecoderCore` transformative-property parsing and application | ISO/IEC 14496-12 section 12.1.4 clean aperture; HEIF image rotation and mirror properties; MIAF section 7.3.6.7 presentation order and section 7.3.9 essential transformative properties | libavif `src/avif.c` clean-aperture conversion, `src/read.c` property parsers and alpha-property validation, and `apps/shared/avifutil.c` transform application at `092276ce89098ead06db80975173191e5fee1826` | Resolve fractional clean-aperture dimensions and center offsets to exact bounded integer pixels, validate the registered rotation/mirror reserved bits, require essential associations, crop after auxiliary-alpha composition, map counter-clockwise HEIF quarter turns to ImageSharp's optimized clockwise rotate modes, then mirror around the signaled axis. Reuse ImageSharp's existing crop, rotation, and flip processors for every pixel type. Retain only the three 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 implemented still-image brands and the bounded `avis`, `hevc`, and `hevx` image-sequence brands, and distinguish item and sequence presentation before parsing their payloads. Layered HEVC and JPEG sequence brands remain unsupported. The decoder validates the complete `ftyp` payload; the fixed-size format detector inspects the available prefix. | +| Bounded HEIF image-sequence parser and writer | ISO/IEC 23008-12:2022 clauses 7 through 10 and HEVC Annex B; ISO/IEC 14496-12 sample tables and normal edit lists; AVIF 1.2 section 3; AV1-ISOBMFF 1.2 section 2 | libavif `src/read.c` track/sample-table paths and `src/write.c` sequence path at `092276ce89098ead06db80975173191e5fee1826`; independently produced HEIC sequence fixtures remain required | Resolve only `pict` master tracks and linked image alpha/metadata state into validated sample descriptors, exact frame durations, sync/dependency state, repetition, presentation transforms, color/HDR properties, and frame metadata. Consume the required container boxes listed in the bounded syntax matrix without exposing a movie, track, edit, or sample-table API. | | `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.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. | diff --git a/src/ImageSharp/Formats/Heif/Heif4CharCode.cs b/src/ImageSharp/Formats/Heif/Heif4CharCode.cs index 94bfe685e..d7205315e 100644 --- a/src/ImageSharp/Formats/Heif/Heif4CharCode.cs +++ b/src/ImageSharp/Formats/Heif/Heif4CharCode.cs @@ -28,6 +28,136 @@ public enum Heif4CharCode : uint /// Mdat = 0x6D646174U, + /// + /// Movie container for an image sequence. + /// + Moov = 0x6D6F6F76U, + + /// + /// Movie header for an image sequence. + /// + Mvhd = 0x6D766864U, + + /// + /// Image sequence track. + /// + Trak = 0x7472616BU, + + /// + /// Image sequence track header. + /// + Tkhd = 0x746B6864U, + + /// + /// Image sequence track references. + /// + Tref = 0x74726566U, + + /// + /// Image sequence edit container. + /// + Edts = 0x65647473U, + + /// + /// Image sequence edit list. + /// + Elst = 0x656C7374U, + + /// + /// Image sequence media container. + /// + Mdia = 0x6D646961U, + + /// + /// Image sequence media header. + /// + Mdhd = 0x6D646864U, + + /// + /// Image sequence media information. + /// + Minf = 0x6D696E66U, + + /// + /// Image sequence sample table. + /// + Stbl = 0x7374626CU, + + /// + /// Image sequence sample descriptions. + /// + Stsd = 0x73747364U, + + /// + /// Image sequence sample timing. + /// + Stts = 0x73747473U, + + /// + /// Image sequence composition offsets. + /// + Ctts = 0x63747473U, + + /// + /// Image sequence composition-to-decode timing. + /// + Cslg = 0x63736C67U, + + /// + /// Image sequence sample-to-chunk map. + /// + Stsc = 0x73747363U, + + /// + /// Image sequence sample sizes. + /// + Stsz = 0x7374737AU, + + /// + /// Compact image sequence sample sizes. + /// + Stz2 = 0x73747A32U, + + /// + /// Image sequence 32-bit chunk offsets. + /// + Stco = 0x7374636FU, + + /// + /// Image sequence 64-bit chunk offsets. + /// + Co64 = 0x636F3634U, + + /// + /// Image sequence sync samples. + /// + Stss = 0x73747373U, + + /// + /// Image sequence sample group descriptions. + /// + Sgpd = 0x73677064U, + + /// + /// Image sequence sample-to-group map. + /// + Sbgp = 0x73626770U, + + /// + /// Image sequence coding constraints. + /// + Ccst = 0x63637374U, + + /// + /// Auxiliary image sequence type. + /// + Auxi = 0x61757869U, + + /// + /// Self-contained data location. + /// + Url = 0x75726C20U, + /// /// Item Information Entry. /// @@ -218,11 +348,36 @@ public enum Heif4CharCode : uint /// Avis = 0x61766973U, + /// + /// AVIF intra-only image sequence brand. + /// + Avio = 0x6176696FU, + + /// + /// HEIF image sequence structural brand. + /// + Msf1 = 0x6D736631U, + + /// + /// ISO base media version 8 structural brand. + /// + Iso8 = 0x69736F38U, + /// /// HEVC-coded image item. /// Hvc1 = 0x68766331U, + /// + /// Layered High Efficiency Coding sample. + /// + Hvc2 = 0x68766332U, + + /// + /// Layered High Efficiency Coding sample. + /// + Lhv1 = 0x6C687631U, + /// /// Legacy JPEG coded tile. /// @@ -318,6 +473,16 @@ public enum Heif4CharCode : uint /// Pict = 0x70696374U, + /// + /// Auxiliary image sequence handler type. + /// + Auxv = 0x61757876U, + + /// + /// Video handler type. + /// + Vide = 0x76696465U, + /// /// Unique Identifier. /// diff --git a/src/ImageSharp/Formats/Heif/Heif4CharCode.tt b/src/ImageSharp/Formats/Heif/Heif4CharCode.tt index badc828fc..f013b50f9 100644 --- a/src/ImageSharp/Formats/Heif/Heif4CharCode.tt +++ b/src/ImageSharp/Formats/Heif/Heif4CharCode.tt @@ -10,6 +10,32 @@ "ftyp", "File Type", "meta", "Metadata", "mdat", "Media Data", + "moov", "Movie container for an image sequence", + "mvhd", "Movie header for an image sequence", + "trak", "Image sequence track", + "tkhd", "Image sequence track header", + "tref", "Image sequence track references", + "edts", "Image sequence edit container", + "elst", "Image sequence edit list", + "mdia", "Image sequence media container", + "mdhd", "Image sequence media header", + "minf", "Image sequence media information", + "stbl", "Image sequence sample table", + "stsd", "Image sequence sample descriptions", + "stts", "Image sequence sample timing", + "ctts", "Image sequence composition offsets", + "cslg", "Image sequence composition-to-decode timing", + "stsc", "Image sequence sample-to-chunk map", + "stsz", "Image sequence sample sizes", + "stz2", "Compact image sequence sample sizes", + "stco", "Image sequence 32-bit chunk offsets", + "co64", "Image sequence 64-bit chunk offsets", + "stss", "Image sequence sync samples", + "sgpd", "Image sequence sample group descriptions", + "sbgp", "Image sequence sample-to-group map", + "ccst", "Image sequence coding constraints", + "auxi", "Auxiliary image sequence type", + "url ", "Self-contained data location", "infe", "Item Information Entry", "idat", "Item Data", "iloc", "Item Location", @@ -48,7 +74,12 @@ "mif1", "High Efficient File brand", "avif", "AVIF brand", "avis", "AVIF image sequence brand", + "avio", "AVIF intra-only image sequence brand", + "msf1", "HEIF image sequence structural brand", + "iso8", "ISO base media version 8 structural brand", "hvc1", "High Efficiency Coding tile", + "hvc2", "Layered High Efficiency Coding sample", + "lhv1", "Layered High Efficiency Coding sample", "jpeg", "Legacy JPEG coded tile", "jpgs", "JPEG image sequence brand", "av01", "AOMedia Video Coding tile", @@ -68,6 +99,8 @@ "mime", "MIME type", "uri ", "URI", "pict", "Picture handler type", + "auxv", "Auxiliary image sequence handler type", + "vide", "Video handler type", "uuid", "Unique Identifier", "free", "Free space", "nclx", "CICP color information", diff --git a/src/ImageSharp/Formats/Heif/HeifBoxReader.cs b/src/ImageSharp/Formats/Heif/HeifBoxReader.cs new file mode 100644 index 000000000..9904bdeb2 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifBoxReader.cs @@ -0,0 +1,223 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Buffers.Binary; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Reads bounded ISO BMFF box headers and payloads used by the HEIF image container. +/// +internal readonly struct HeifBoxReader +{ + /// + /// The allocator used for payloads that must be materialized while parsing. + /// + private readonly MemoryAllocator allocator; + + /// + /// Initializes a new instance of the struct. + /// + /// The allocator used for bounded payload buffers. + public HeifBoxReader(MemoryAllocator allocator) => this.allocator = allocator; + + /// + /// Reads an ISO BMFF box header using caller-owned scratch and resolves its validated payload length. + /// + /// The stream positioned at the box size field. + /// The absolute end position of the containing box or file. + /// Caller-owned scratch containing at least eight bytes. + /// Receives the box four-character code. + /// Indicates whether a size-zero box may extend to the end of the file. + /// The number of payload bytes following the complete variable-length header. + public static long ReadHeader(Stream stream, long parentEndPosition, Span scratch, out Heif4CharCode boxType, bool topLevel = false) + { + if (parentEndPosition - stream.Position < 8) + { + throw new InvalidImageContentException("Not enough data to read the box header."); + } + + Span buffer = scratch[..8]; + ReadExactly(stream, buffer, "Not enough data to read the box header."); + + ulong boxSize = BinaryPrimitives.ReadUInt32BigEndian(buffer); + int headerSize = 8; + boxType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(buffer[4..]); + + if (boxSize == 1) + { + if (parentEndPosition - stream.Position < 8) + { + throw new InvalidImageContentException("Not enough data to read the extended box size."); + } + + ReadExactly(stream, buffer, "Not enough data to read the extended box size."); + boxSize = BinaryPrimitives.ReadUInt64BigEndian(buffer); + headerSize += 8; + } + + if (boxType == Heif4CharCode.Uuid) + { + if (parentEndPosition - stream.Position < 16) + { + throw new InvalidImageContentException("Not enough data to read the UUID box user type."); + } + + // The UUID user type belongs to the variable box header even though the bounded image parser does not + // interpret it. Advance here so every caller receives the actual payload start and length. + Skip(stream, 16); + headerSize += 16; + } + + if (boxSize == 0) + { + if (!topLevel) + { + throw new InvalidImageContentException("A nested box cannot extend to the end of the file."); + } + + return parentEndPosition - stream.Position; + } + + if (boxSize < (ulong)headerSize) + { + throw new InvalidImageContentException("Box size is smaller than its header."); + } + + ulong contentLength = boxSize - (ulong)headerSize; + if (contentLength > (ulong)(parentEndPosition - stream.Position)) + { + throw new InvalidImageContentException("Box size extends beyond its parent boundary."); + } + + return (long)contentLength; + } + + /// + /// Parses an ISO BMFF child-box header from a bounded parent payload. + /// + /// The remaining bytes in the parent payload, beginning at the child size field. + /// Receives the validated child payload length. + /// Receives the child box four-character code. + /// The number of bytes occupied by the complete child header. + public static int ParseHeader(ReadOnlySpan buffer, out long length, out Heif4CharCode boxType) + { + if (buffer.Length < 8) + { + throw new InvalidImageContentException("Not enough data to read the box header."); + } + + ulong boxSize = BinaryPrimitives.ReadUInt32BigEndian(buffer); + int bytesRead = 8; + boxType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(buffer[4..]); + if (boxSize == 1) + { + if (buffer.Length < 16) + { + throw new InvalidImageContentException("Not enough data to read the extended box size."); + } + + boxSize = BinaryPrimitives.ReadUInt64BigEndian(buffer[bytesRead..]); + bytesRead += 8; + } + + if (boxType == Heif4CharCode.Uuid) + { + if (buffer.Length - bytesRead < 16) + { + throw new InvalidImageContentException("Not enough data to read the UUID box user type."); + } + + bytesRead += 16; + } + + if (boxSize == 0) + { + throw new InvalidImageContentException("A nested box cannot extend to the end of the file."); + } + + if (boxSize < (ulong)bytesRead) + { + throw new InvalidImageContentException("Box size is smaller than its header."); + } + + ulong contentLength = boxSize - (ulong)bytesRead; + if (contentLength > (ulong)(buffer.Length - bytesRead)) + { + throw new InvalidImageContentException("Box size extends beyond its parent boundary."); + } + + length = (long)contentLength; + return bytesRead; + } + + /// + /// Reads a complete bounded box payload into allocator-owned memory. + /// + /// The stream positioned at the payload start. + /// The validated payload length. + /// An owner containing exactly the requested payload bytes. + public IMemoryOwner ReadPayload(Stream stream, long length) + { + if ((ulong)length > int.MaxValue) + { + throw new InvalidImageContentException("Box content is too large to buffer."); + } + + int bufferLength = (int)length; + IMemoryOwner memory = this.allocator.Allocate(bufferLength); + try + { + ReadExactly(stream, memory.GetSpan(), "Stream length is not sufficient for box content."); + return memory; + } + catch + { + memory.Dispose(); + throw; + } + } + + /// + /// Advances over a validated box payload without narrowing its 64-bit length. + /// + /// The seekable container stream. + /// The validated payload length. + public static void Skip(Stream stream, long length) => stream.Seek(length, SeekOrigin.Current); + + /// + /// Validates a child payload length against the bytes remaining in its parent. + /// + /// The declared child payload length. + /// The number of bytes remaining in the parent. + public static void EnsureInsideParent(long length, long parentLength) + { + if (length < 0 || parentLength < 0 || length > parentLength) + { + throw new InvalidImageContentException("Box size extends beyond its parent boundary."); + } + } + + /// + /// Reads exactly the requested number of bytes or rejects the truncated payload. + /// + /// The source stream. + /// The complete destination span. + /// The malformed-image message used when the stream ends early. + public static void ReadExactly(Stream stream, Span destination, string message) + { + int offset = 0; + while (offset < destination.Length) + { + int read = stream.Read(destination[offset..]); + if (read == 0) + { + throw new InvalidImageContentException(message); + } + + offset += read; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs index d6c4ef2c5..722b93834 100644 --- a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs +++ b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs @@ -54,6 +54,16 @@ internal sealed class HeifDecoderCore : ImageDecoderCore /// private readonly ImageMetadata metadata; + /// + /// The shared bounded box reader used by the item and image-sequence container paths. + /// + private readonly HeifBoxReader boxReader; + + /// + /// The fixed scratch buffer reused for all item-container box headers in this decode operation. + /// + private readonly byte[] boxHeaderScratch; + /// /// The item identifier selected by the primary-item box. /// @@ -88,6 +98,8 @@ internal sealed class HeifDecoderCore : ImageDecoderCore { this.configuration = options.Configuration; this.metadata = new ImageMetadata(); + this.boxReader = new HeifBoxReader(this.configuration.MemoryAllocator); + this.boxHeaderScratch = new byte[8]; this.items = []; this.itemLinks = []; } @@ -109,7 +121,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore // the metadata box. Complete the top-level scan before resolving and decoding the primary item. while (stream.Position < stream.Length) { - long boxLength = this.ReadBoxHeader(stream, stream.Length, out Heif4CharCode boxType, true); + long boxLength = HeifBoxReader.ReadHeader(stream, stream.Length, this.boxHeaderScratch, out Heif4CharCode boxType, true); switch (boxType) { case Heif4CharCode.Meta: @@ -117,14 +129,14 @@ internal sealed class HeifDecoderCore : ImageDecoderCore break; case Heif4CharCode.Mdat: case Heif4CharCode.Free: - SkipBox(stream, boxLength); + HeifBoxReader.Skip(stream, boxLength); break; case 0U: // Some files have trailing zeros, skiping to EOF. - SkipBox(stream, stream.Length - stream.Position); + HeifBoxReader.Skip(stream, stream.Length - stream.Position); break; default: - SkipBox(stream, boxLength); + HeifBoxReader.Skip(stream, boxLength); break; } } @@ -149,7 +161,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore // metadata come from item declarations and associated properties rather than reconstructed pixels. while (stream.Position < stream.Length) { - long boxLength = this.ReadBoxHeader(stream, stream.Length, out Heif4CharCode boxType, true); + long boxLength = HeifBoxReader.ReadHeader(stream, stream.Length, this.boxHeaderScratch, out Heif4CharCode boxType, true); switch (boxType) { case Heif4CharCode.Meta: @@ -157,7 +169,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore break; default: // Silently skip all other box types. - SkipBox(stream, boxLength); + HeifBoxReader.Skip(stream, boxLength); break; } } @@ -181,7 +193,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore /// when the complete file-type payload advertises a supported still-image brand. private bool CheckFileTypeBox(BufferedReadStream stream) { - long boxLength = this.ReadBoxHeader(stream, stream.Length, out Heif4CharCode boxType, true); + long boxLength = HeifBoxReader.ReadHeader(stream, stream.Length, this.boxHeaderScratch, out Heif4CharCode boxType, true); if (boxType != Heif4CharCode.Ftyp) { return false; @@ -192,7 +204,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore return false; } - using IMemoryOwner boxMemory = this.ReadIntoBuffer(stream, boxLength); + using IMemoryOwner boxMemory = this.boxReader.ReadPayload(stream, boxLength); Span boxBuffer = boxMemory.GetSpan(); return HeifConstants.IsSupportedFileType(boxBuffer); } @@ -300,145 +312,6 @@ internal sealed class HeifDecoderCore : ImageDecoderCore } } - /// - /// Reads an ISO BMFF box header and resolves its validated payload length. - /// - /// The stream positioned at the box size field. - /// The absolute end position of the containing box or file. - /// Receives the box four-character code. - /// Indicates whether a size-zero box may extend to the end of the file. - /// The number of payload bytes following the complete variable-length header. - private long ReadBoxHeader(BufferedReadStream stream, long parentEndPosition, out Heif4CharCode boxType, bool topLevel = false) - { - if (parentEndPosition - stream.Position < 8) - { - throw new InvalidImageContentException("Not enough data to read the box header."); - } - - Span buf = stackalloc byte[8]; - int bytesRead = stream.Read(buf); - if (bytesRead != 8) - { - throw new InvalidImageContentException("Not enough data to read the box header."); - } - - ulong boxSize = BinaryPrimitives.ReadUInt32BigEndian(buf); - int headerSize = 8; - boxType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(buf[4..]); - - if (boxSize == 1) - { - // A 32-bit size value of one replaces the size field with the following unsigned 64-bit largesize value. - if (parentEndPosition - stream.Position < 8) - { - throw new InvalidImageContentException("Not enough data to read the extended box size."); - } - - bytesRead = stream.Read(buf); - if (bytesRead != 8) - { - throw new InvalidImageContentException("Not enough data to read the extended box size."); - } - - boxSize = BinaryPrimitives.ReadUInt64BigEndian(buf); - headerSize += 8; - } - - if (boxType == Heif4CharCode.Uuid) - { - if (parentEndPosition - stream.Position < 16) - { - throw new InvalidImageContentException("Not enough data to read the UUID box user type."); - } - - // The UUID user type is part of the variable-sized box header, even though this decoder skips its value. - SkipBox(stream, 16); - headerSize += 16; - } - - if (boxSize == 0) - { - // ISO BMFF permits a size-zero box only at file level, where it consumes the rest of the file. - if (!topLevel) - { - throw new InvalidImageContentException("A nested box cannot extend to the end of the file."); - } - - return parentEndPosition - stream.Position; - } - - if (boxSize < (ulong)headerSize) - { - throw new InvalidImageContentException("Box size is smaller than its header."); - } - - ulong contentLength = boxSize - (ulong)headerSize; - if (contentLength > (ulong)(parentEndPosition - stream.Position)) - { - throw new InvalidImageContentException("Box size extends beyond its parent boundary."); - } - - return (long)contentLength; - } - - /// - /// Parses an ISO BMFF child-box header from a bounded parent payload. - /// - /// The remaining bytes in the parent payload, beginning at the child size field. - /// Receives the validated child payload length. - /// Receives the child box four-character code. - /// The number of bytes occupied by the complete child header. - private static int ParseBoxHeader(Span buffer, out long length, out Heif4CharCode boxType) - { - if (buffer.Length < 8) - { - throw new InvalidImageContentException("Not enough data to read the box header."); - } - - ulong boxSize = BinaryPrimitives.ReadUInt32BigEndian(buffer); - int bytesRead = 8; - boxType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(buffer[4..]); - if (boxSize == 1) - { - if (buffer.Length < 16) - { - throw new InvalidImageContentException("Not enough data to read the extended box size."); - } - - boxSize = BinaryPrimitives.ReadUInt64BigEndian(buffer[bytesRead..]); - bytesRead += 8; - } - - if (boxType == Heif4CharCode.Uuid) - { - if (buffer.Length - bytesRead < 16) - { - throw new InvalidImageContentException("Not enough data to read the UUID box user type."); - } - - bytesRead += 16; - } - - if (boxSize == 0) - { - throw new InvalidImageContentException("A nested box cannot extend to the end of the file."); - } - - if (boxSize < (ulong)bytesRead) - { - throw new InvalidImageContentException("Box size is smaller than its header."); - } - - ulong contentLength = boxSize - (ulong)bytesRead; - if (contentLength > (ulong)(buffer.Length - bytesRead)) - { - throw new InvalidImageContentException("Box size extends beyond its parent boundary."); - } - - length = (long)contentLength; - return bytesRead; - } - /// /// Indexes and parses the recognized children of a metadata box. /// @@ -459,7 +332,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore Dictionary boxes = []; while (stream.Position < endPosition) { - long length = this.ReadBoxHeader(stream, endPosition, out Heif4CharCode boxType); + long length = HeifBoxReader.ReadHeader(stream, endPosition, this.boxHeaderScratch, out Heif4CharCode boxType); if (Array.IndexOf(MetadataParseOrder, boxType) >= 0) { // Association and location boxes can precede the item declarations they reference. @@ -469,7 +342,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore } } - SkipBox(stream, length); + HeifBoxReader.Skip(stream, length); } foreach (Heif4CharCode boxType in MetadataParseOrder) @@ -523,7 +396,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore /// The bounded handler payload length. private void ParseHandler(BufferedReadStream stream, long boxLength) { - using IMemoryOwner boxMemory = this.ReadIntoBuffer(stream, boxLength); + using IMemoryOwner boxMemory = this.boxReader.ReadPayload(stream, boxLength); Span boxBuffer = boxMemory.GetSpan(); EnsureBufferRemaining(boxBuffer, 0, 12, "handler"); @@ -545,7 +418,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore /// The bounded item-information payload length. private void ParseItemInfo(BufferedReadStream stream, long boxLength) { - using IMemoryOwner boxMemory = this.ReadIntoBuffer(stream, boxLength); + using IMemoryOwner boxMemory = this.boxReader.ReadPayload(stream, boxLength); Span boxBuffer = boxMemory.GetSpan(); EnsureBufferRemaining(boxBuffer, 0, 4, "item info"); @@ -577,7 +450,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore /// The complete item-information-entry box length. private int ParseItemInfoEntry(Span buffer) { - int headerLength = ParseBoxHeader(buffer, out long boxLength, out Heif4CharCode boxType); + int headerLength = HeifBoxReader.ParseHeader(buffer, out long boxLength, out Heif4CharCode boxType); if (boxType != Heif4CharCode.Infe) { throw new InvalidImageContentException($"The item info box contains unexpected child '{boxType}'."); @@ -699,7 +572,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore /// The bounded item-reference payload length. private void ParseItemReference(BufferedReadStream stream, long boxLength) { - using IMemoryOwner boxMemory = this.ReadIntoBuffer(stream, boxLength); + using IMemoryOwner boxMemory = this.boxReader.ReadPayload(stream, boxLength); Span boxBuffer = boxMemory.GetSpan(); EnsureBufferRemaining(boxBuffer, 0, 4, "item reference"); @@ -714,7 +587,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore bytesRead += 4; while (bytesRead < boxLength) { - int referenceHeaderLength = ParseBoxHeader(boxBuffer[bytesRead..], out long referenceLength, out Heif4CharCode linkType); + int referenceHeaderLength = HeifBoxReader.ParseHeader(boxBuffer[bytesRead..], out long referenceLength, out Heif4CharCode linkType); int referenceEnd = checked(bytesRead + referenceHeaderLength + (int)referenceLength); Span referenceBuffer = boxBuffer[..referenceEnd]; bytesRead += referenceHeaderLength; @@ -756,7 +629,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore /// The bounded primary-item payload length. private void ParsePrimaryItem(BufferedReadStream stream, long boxLength) { - using IMemoryOwner boxMemory = this.ReadIntoBuffer(stream, boxLength); + using IMemoryOwner boxMemory = this.boxReader.ReadPayload(stream, boxLength); Span boxBuffer = boxMemory.GetSpan(); EnsureBufferRemaining(boxBuffer, 0, 4, "primary item"); @@ -789,7 +662,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore List<(long Offset, long Length)> associations = []; while (stream.Position < endBoxPosition) { - long containerLength = this.ReadBoxHeader(stream, endBoxPosition, out Heif4CharCode containerType); + long containerLength = HeifBoxReader.ReadHeader(stream, endBoxPosition, this.boxHeaderScratch, out Heif4CharCode containerType); if (containerType == Heif4CharCode.Ipco) { if (propertyContainer.HasValue) @@ -805,7 +678,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore } // Unknown optional children remain bounded by iprp and do not expand the still-image model. - SkipBox(stream, containerLength); + HeifBoxReader.Skip(stream, containerLength); } if (!propertyContainer.HasValue) @@ -835,8 +708,8 @@ internal sealed class HeifDecoderCore : ImageDecoderCore long endPosition = stream.Position + boxLength; while (stream.Position < endPosition) { - long itemLength = this.ReadBoxHeader(stream, endPosition, out Heif4CharCode itemType); - using IMemoryOwner boxMemory = this.ReadIntoBuffer(stream, itemLength); + long itemLength = HeifBoxReader.ReadHeader(stream, endPosition, this.boxHeaderScratch, out Heif4CharCode itemType); + using IMemoryOwner boxMemory = this.boxReader.ReadPayload(stream, itemLength); Span boxBuffer = boxMemory.GetSpan(); switch (itemType) { @@ -1345,7 +1218,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore /// The properties in the order used by association indices. private void ParsePropertyAssociation(BufferedReadStream stream, long boxLength, List> properties) { - using IMemoryOwner boxMemory = this.ReadIntoBuffer(stream, boxLength); + using IMemoryOwner boxMemory = this.boxReader.ReadPayload(stream, boxLength); Span boxBuffer = boxMemory.GetSpan(); EnsureBufferRemaining(boxBuffer, 0, 8, "item property association"); byte version = boxBuffer[0]; @@ -1593,7 +1466,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore /// The bounded item-location payload length. private void ParseItemLocation(BufferedReadStream stream, long boxLength) { - using IMemoryOwner boxMemory = this.ReadIntoBuffer(stream, boxLength); + using IMemoryOwner boxMemory = this.boxReader.ReadPayload(stream, boxLength); Span boxBuffer = boxMemory.GetSpan(); int bytesRead = 0; EnsureBufferRemaining(boxBuffer, bytesRead, 6, "item location"); @@ -1826,7 +1699,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore throw new InvalidImageContentException($"Item {item.Id} uses an unsupported location origin."); } - EnsureBoxInsideParent(loc.Length, sourceBytesRemaining); + HeifBoxReader.EnsureInsideParent(loc.Length, sourceBytesRemaining); stream.Position = sourceOffset; int extentLength = (int)loc.Length; int bytesRead = stream.Read(itemBuffer.Slice(writeOffset, extentLength)); @@ -2348,59 +2221,6 @@ internal sealed class HeifDecoderCore : ImageDecoderCore } } - /// - /// Advances over a box payload without narrowing its 64-bit length. - /// - /// The seekable container stream. - /// The validated payload length. - private static void SkipBox(Stream stream, long boxLength) - => stream.Seek(boxLength, SeekOrigin.Current); - - /// - /// Reads a complete bounded box payload into allocator-owned memory. - /// - /// The stream positioned at the payload start. - /// The validated payload length. - /// An owner containing exactly the requested payload bytes. - private IMemoryOwner ReadIntoBuffer(Stream stream, long length) - { - if ((ulong)length > int.MaxValue) - { - throw new InvalidImageContentException("Box content is too large to buffer."); - } - - int bufferLength = (int)length; - IMemoryOwner buffer = this.configuration.MemoryAllocator.Allocate(bufferLength); - int bytesRead = stream.Read(buffer.GetSpan()); - if (bytesRead != bufferLength) - { - throw new InvalidImageContentException("Stream length is not sufficient for box content."); - } - - return buffer; - } - - /// - /// Validates a box payload length against the bytes remaining in the file. - /// - /// The declared box payload length. - /// The stream positioned at the payload start. - private static void EnsureBoxBoundary(long boxLength, Stream stream) - => EnsureBoxInsideParent(boxLength, stream.Length - stream.Position); - - /// - /// Validates a child payload length against its remaining parent payload. - /// - /// The declared child payload length. - /// The number of bytes remaining in the parent. - private static void EnsureBoxInsideParent(long boxLength, long parentLength) - { - if (boxLength < 0 || parentLength < 0 || boxLength > parentLength) - { - throw new InvalidImageContentException("Box size extends beyond its parent boundary."); - } - } - /// /// Finds an item by its file-defined identifier. /// diff --git a/src/ImageSharp/Formats/Heif/HeifSequence.cs b/src/ImageSharp/Formats/Heif/HeifSequence.cs new file mode 100644 index 000000000..14938b3ce --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifSequence.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Contains the selected color and optional alpha tracks of one HEIF image sequence. +/// +internal sealed class HeifSequence +{ + /// + /// Initializes a new instance of the class. + /// + /// The selected master image-sequence track. + /// The linked alpha image-sequence track, when present. + /// The movie time scale in units per second. + public HeifSequence(HeifSequenceTrack colorTrack, HeifSequenceTrack? alphaTrack, uint movieTimescale) + { + this.ColorTrack = colorTrack; + this.AlphaTrack = alphaTrack; + this.MovieTimescale = movieTimescale; + } + + /// + /// Gets the selected master image-sequence track. + /// + public HeifSequenceTrack ColorTrack { get; } + + /// + /// Gets the linked alpha image-sequence track, when present. + /// + public HeifSequenceTrack? AlphaTrack { get; } + + /// + /// Gets the movie time scale in units per second. + /// + public uint MovieTimescale { get; } +} diff --git a/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs b/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs new file mode 100644 index 000000000..ff9e71114 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs @@ -0,0 +1,1791 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Buffers.Binary; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Hevc; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Parses the bounded track and sample syntax required to identify a HEIF image sequence. +/// +internal sealed class HeifSequenceParser +{ + /// + /// The reusable scratch size used by sequential table reads. + /// + private const int ScratchLength = 4096; + + /// + /// The configured allocator used for parser scratch and bounded table state. + /// + private readonly MemoryAllocator allocator; + + /// + /// The shared reader for variable-length HEIF box headers. + /// + private readonly HeifBoxReader boxReader; + + /// + /// The maximum number of sample descriptors retained for decoding or identification. + /// + private readonly int maxFrames; + + /// + /// Initializes a new instance of the class. + /// + /// The configured ImageSharp memory allocator. + /// The maximum number of image-sequence frames to retain. + public HeifSequenceParser(MemoryAllocator allocator, uint maxFrames) + { + this.allocator = allocator; + this.boxReader = new HeifBoxReader(allocator); + this.maxFrames = (int)maxFrames; + } + + /// + /// Parses one movie box and selects its master image-sequence track and linked alpha track. + /// + /// The seekable HEIF stream positioned at the movie payload. + /// The validated movie payload length. + /// The bounded image-sequence model required by the HEIF decoder. + public HeifSequence Parse(Stream stream, long boxLength) + { + long movieStart = stream.Position; + long movieEnd = checked(movieStart + boxLength); + HeifBoxReader.EnsureInsideParent(boxLength, stream.Length - movieStart); + + using IMemoryOwner scratchOwner = this.allocator.Allocate(ScratchLength); + Span scratch = scratchOwner.GetSpan(); + BoxReference movieHeader = default; + uint colorTrackId = 0; + + // The first pass reads only fixed track identity fields. This prevents files containing unrelated media tracks + // from forcing codec configurations and sample tables into the image decoder's retained model. + while (stream.Position < movieEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, movieEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + if (childType == Heif4CharCode.Mvhd) + { + SetUnique(ref movieHeader, childStart, childLength, "movie", childType); + } + else if (childType == Heif4CharCode.Trak) + { + TrackIdentity identity = ScanTrackIdentity(stream, childLength, scratch); + if (colorTrackId == 0 + && identity.IsEnabledInMovie + && identity.HandlerType == Heif4CharCode.Pict + && identity.AuxiliaryForTrackId == 0) + { + colorTrackId = identity.Id; + } + } + + stream.Position = checked(childStart + childLength); + } + + if (!movieHeader.IsPresent) + { + throw new InvalidImageContentException("The HEIF image sequence has no movie header."); + } + + if (colorTrackId == 0) + { + throw new InvalidImageContentException("The HEIF image sequence has no enabled picture track."); + } + + stream.Position = movieHeader.Offset; + uint movieTimescale = ParseMovieHeader(stream, movieHeader.Length, scratch); + HeifSequenceTrack? colorTrack = null; + HeifSequenceTrack? alphaTrack = null; + + // Track references can precede the master track. Re-scan the bounded movie now that the selected master ID is + // known, and fully parse only that track and the one alpha auxiliary linked to it. + stream.Position = movieStart; + while (stream.Position < movieEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, movieEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + if (childType == Heif4CharCode.Trak) + { + TrackIdentity identity = ScanTrackIdentity(stream, childLength, scratch); + bool isColor = identity.Id == colorTrackId; + bool isLinkedAuxiliary = identity.AuxiliaryForTrackId == colorTrackId + && identity.HandlerType is Heif4CharCode.Auxv or Heif4CharCode.Pict; + + if (isColor || (isLinkedAuxiliary && alphaTrack is null)) + { + stream.Position = childStart; + HeifSequenceTrack track = this.ParseTrack(stream, childLength, identity, scratch); + if (isColor) + { + colorTrack = track; + } + else if (track.IsAlpha) + { + alphaTrack = track; + } + } + } + + stream.Position = checked(childStart + childLength); + } + + if (colorTrack is null) + { + throw new InvalidImageContentException("The selected HEIF picture track could not be parsed."); + } + + ValidateAlphaTrack(colorTrack, alphaTrack); + return new HeifSequence(colorTrack, alphaTrack, movieTimescale); + } + + /// + /// Reads the fixed identity fields used to select an image track without materializing its sample table. + /// + /// The stream positioned at the track payload. + /// The validated track payload length. + /// The parser-owned reusable scratch span. + /// The track identity and image relationship fields. + private static TrackIdentity ScanTrackIdentity(Stream stream, long boxLength, Span scratch) + { + long trackEnd = checked(stream.Position + boxLength); + BoxReference trackHeader = default; + BoxReference trackReferences = default; + BoxReference media = default; + + while (stream.Position < trackEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, trackEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + switch (childType) + { + case Heif4CharCode.Tkhd: + SetUnique(ref trackHeader, childStart, childLength, "track", childType); + break; + case Heif4CharCode.Tref: + SetUnique(ref trackReferences, childStart, childLength, "track", childType); + break; + case Heif4CharCode.Mdia: + SetUnique(ref media, childStart, childLength, "track", childType); + break; + } + + stream.Position = checked(childStart + childLength); + } + + if (!trackHeader.IsPresent || !media.IsPresent) + { + throw new InvalidImageContentException("A HEIF image-sequence track is missing its header or media box."); + } + + stream.Position = trackHeader.Offset; + TrackIdentity identity = ParseTrackHeader(stream, trackHeader.Length, scratch); + if (trackReferences.IsPresent) + { + stream.Position = trackReferences.Offset; + ParseTrackReferences(stream, trackReferences.Length, ref identity, scratch); + } + + stream.Position = media.Offset; + identity.HandlerType = ScanMediaHandler(stream, media.Length, scratch); + return identity; + } + + /// + /// Parses the retained behavior of one selected image-sequence track. + /// + /// The stream positioned at the track payload. + /// The validated track payload length. + /// The fixed identity fields from the selection pass. + /// The parser-owned reusable scratch span. + /// The selected track with its validated sample descriptors. + private HeifSequenceTrack ParseTrack(Stream stream, long boxLength, TrackIdentity identity, Span scratch) + { + long trackEnd = checked(stream.Position + boxLength); + BoxReference edit = default; + BoxReference media = default; + + while (stream.Position < trackEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, trackEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + if (childType == Heif4CharCode.Edts) + { + SetUnique(ref edit, childStart, childLength, "track", childType); + } + else if (childType == Heif4CharCode.Mdia) + { + SetUnique(ref media, childStart, childLength, "track", childType); + } + + stream.Position = checked(childStart + childLength); + } + + if (!media.IsPresent) + { + throw new InvalidImageContentException("A selected HEIF image-sequence track has no media box."); + } + + HeifSequenceTrack track = new() + { + Id = identity.Id, + Width = identity.Width, + Height = identity.Height, + Matrix = identity.Matrix, + HandlerType = identity.HandlerType, + TrackDuration = identity.TrackDuration, + AuxiliaryForTrackId = identity.AuxiliaryForTrackId, + IsPremultiplied = identity.PremultipliedByTrackId != 0 + }; + + if (edit.IsPresent) + { + stream.Position = edit.Offset; + ParseEdit(stream, edit.Length, track, scratch); + } + + stream.Position = media.Offset; + this.ParseMedia(stream, media.Length, track, scratch); + if (track.Matrix.HasPerspective) + { + throw new InvalidImageContentException("The HEIF image-sequence track uses an unsupported perspective matrix."); + } + + if (track.TotalSampleCount == 0 || track.Samples.Length == 0) + { + throw new InvalidImageContentException("The HEIF image-sequence track contains no retained image samples."); + } + + return track; + } + + /// + /// Parses the movie time scale required to interpret track edit durations. + /// + /// The stream positioned at the movie-header payload. + /// The validated movie-header payload length. + /// The parser-owned reusable scratch span. + /// The nonzero movie time scale. + private static uint ParseMovieHeader(Stream stream, long boxLength, Span scratch) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 4, "movie header"); + byte version = prefix[0]; + int requiredLength = version switch + { + 0 => 20, + 1 => 32, + _ => throw new InvalidImageContentException($"The movie header has unsupported version {version}.") + }; + + prefix = ReadPrefixFromStart(stream, boxLength, scratch, requiredLength, "movie header"); + EnsureZeroFlags(prefix, "movie header"); + uint timescale = BinaryPrimitives.ReadUInt32BigEndian(prefix[(version == 0 ? 12 : 20)..]); + if (timescale == 0) + { + throw new InvalidImageContentException("The movie header has a zero time scale."); + } + + return timescale; + } + + /// + /// Parses track identity, dimensions, duration, and transformation matrix. + /// + /// The stream positioned at the track-header payload. + /// The validated track-header payload length. + /// The parser-owned reusable scratch span. + /// The fixed track identity fields. + private static TrackIdentity ParseTrackHeader(Stream stream, long boxLength, Span scratch) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 4, "track header"); + byte version = prefix[0]; + int requiredLength = version switch + { + 0 => 84, + 1 => 96, + _ => throw new InvalidImageContentException($"The track header has unsupported version {version}.") + }; + + prefix = ReadPrefixFromStart(stream, boxLength, scratch, requiredLength, "track header"); + uint flags = ReadFlags(prefix); + int trackIdOffset = version == 0 ? 12 : 20; + int durationOffset = version == 0 ? 20 : 28; + int matrixOffset = version == 0 ? 40 : 52; + int widthOffset = version == 0 ? 76 : 88; + uint id = BinaryPrimitives.ReadUInt32BigEndian(prefix[trackIdOffset..]); + if (id == 0) + { + throw new InvalidImageContentException("A HEIF image-sequence track has identifier zero."); + } + + ulong duration = version == 0 + ? BinaryPrimitives.ReadUInt32BigEndian(prefix[durationOffset..]) + : BinaryPrimitives.ReadUInt64BigEndian(prefix[durationOffset..]); + + if (version == 0 && duration == uint.MaxValue) + { + duration = ulong.MaxValue; + } + + int width = checked((int)(BinaryPrimitives.ReadUInt32BigEndian(prefix[widthOffset..]) >> 16)); + int height = checked((int)(BinaryPrimitives.ReadUInt32BigEndian(prefix[(widthOffset + 4)..]) >> 16)); + if (width == 0 || height == 0) + { + throw new InvalidImageContentException("A HEIF image-sequence track has zero dimensions."); + } + + HeifTrackMatrix matrix = new( + BinaryPrimitives.ReadInt32BigEndian(prefix[matrixOffset..]), + BinaryPrimitives.ReadInt32BigEndian(prefix[(matrixOffset + 4)..]), + BinaryPrimitives.ReadInt32BigEndian(prefix[(matrixOffset + 8)..]), + BinaryPrimitives.ReadInt32BigEndian(prefix[(matrixOffset + 12)..]), + BinaryPrimitives.ReadInt32BigEndian(prefix[(matrixOffset + 16)..]), + BinaryPrimitives.ReadInt32BigEndian(prefix[(matrixOffset + 20)..]), + BinaryPrimitives.ReadInt32BigEndian(prefix[(matrixOffset + 24)..]), + BinaryPrimitives.ReadInt32BigEndian(prefix[(matrixOffset + 28)..]), + BinaryPrimitives.ReadInt32BigEndian(prefix[(matrixOffset + 32)..])); + + return new TrackIdentity(id, (flags & 3) == 3, width, height, duration, matrix); + } + + /// + /// Parses the image-specific track references used for alpha and premultiplication linkage. + /// + /// The stream positioned at the track-reference payload. + /// The validated track-reference payload length. + /// The track identity receiving image relationships. + /// The parser-owned reusable scratch span. + private static void ParseTrackReferences(Stream stream, long boxLength, ref TrackIdentity identity, Span scratch) + { + long referenceEnd = checked(stream.Position + boxLength); + while (stream.Position < referenceEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, referenceEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + if (childType is Heif4CharCode.Auxl or Heif4CharCode.Prem) + { + if (childLength < 4 || (childLength & 3) != 0) + { + throw new InvalidImageContentException($"The '{childType}' track reference has an invalid identifier list."); + } + + ReadOnlySpan data = ReadPrefix(stream, childLength, scratch, 4, $"{childType} track reference"); + uint referencedTrackId = BinaryPrimitives.ReadUInt32BigEndian(data); + if (referencedTrackId == 0) + { + throw new InvalidImageContentException($"The '{childType}' track reference contains identifier zero."); + } + + if (childType == Heif4CharCode.Auxl) + { + if (identity.AuxiliaryForTrackId != 0) + { + throw new InvalidImageContentException("The track contains duplicate alpha-auxiliary references."); + } + + identity.AuxiliaryForTrackId = referencedTrackId; + } + else + { + if (identity.PremultipliedByTrackId != 0) + { + throw new InvalidImageContentException("The track contains duplicate premultiplication references."); + } + + identity.PremultipliedByTrackId = referencedTrackId; + } + } + + stream.Position = checked(childStart + childLength); + } + } + + /// + /// Finds and parses the handler type from one media box. + /// + /// The stream positioned at the media payload. + /// The validated media payload length. + /// The parser-owned reusable scratch span. + /// The declared media handler type. + private static Heif4CharCode ScanMediaHandler(Stream stream, long boxLength, Span scratch) + { + long mediaEnd = checked(stream.Position + boxLength); + BoxReference handler = default; + while (stream.Position < mediaEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, mediaEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + if (childType == Heif4CharCode.Hdlr) + { + SetUnique(ref handler, childStart, childLength, "media", childType); + } + + stream.Position = checked(childStart + childLength); + } + + if (!handler.IsPresent) + { + throw new InvalidImageContentException("A HEIF image-sequence media box has no handler."); + } + + stream.Position = handler.Offset; + return ParseHandler(stream, handler.Length, scratch); + } + + /// + /// Parses the media header and sample table of a selected image track. + /// + /// The stream positioned at the media payload. + /// The validated media payload length. + /// The selected track receiving media state. + /// The parser-owned reusable scratch span. + private void ParseMedia(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + long mediaEnd = checked(stream.Position + boxLength); + BoxReference mediaHeader = default; + BoxReference handler = default; + BoxReference mediaInformation = default; + + while (stream.Position < mediaEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, mediaEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + switch (childType) + { + case Heif4CharCode.Mdhd: + SetUnique(ref mediaHeader, childStart, childLength, "media", childType); + break; + case Heif4CharCode.Hdlr: + SetUnique(ref handler, childStart, childLength, "media", childType); + break; + case Heif4CharCode.Minf: + SetUnique(ref mediaInformation, childStart, childLength, "media", childType); + break; + } + + stream.Position = checked(childStart + childLength); + } + + if (!mediaHeader.IsPresent || !handler.IsPresent || !mediaInformation.IsPresent) + { + throw new InvalidImageContentException("A selected HEIF image-sequence media box is incomplete."); + } + + stream.Position = mediaHeader.Offset; + ParseMediaHeader(stream, mediaHeader.Length, track, scratch); + stream.Position = handler.Offset; + Heif4CharCode handlerType = ParseHandler(stream, handler.Length, scratch); + if (handlerType != track.HandlerType) + { + throw new InvalidImageContentException("The HEIF image-sequence track handler changed between parser passes."); + } + + stream.Position = mediaInformation.Offset; + this.ParseMediaInformation(stream, mediaInformation.Length, track, scratch); + } + + /// + /// Parses the media time scale and duration of a selected track. + /// + /// The stream positioned at the media-header payload. + /// The validated media-header payload length. + /// The selected track receiving timing state. + /// The parser-owned reusable scratch span. + private static void ParseMediaHeader(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 4, "media header"); + byte version = prefix[0]; + int requiredLength = version switch + { + 0 => 24, + 1 => 36, + _ => throw new InvalidImageContentException($"The media header has unsupported version {version}.") + }; + + prefix = ReadPrefixFromStart(stream, boxLength, scratch, requiredLength, "media header"); + EnsureZeroFlags(prefix, "media header"); + int timescaleOffset = version == 0 ? 12 : 20; + track.MediaTimescale = BinaryPrimitives.ReadUInt32BigEndian(prefix[timescaleOffset..]); + track.MediaDuration = version == 0 + ? BinaryPrimitives.ReadUInt32BigEndian(prefix[(timescaleOffset + 4)..]) + : BinaryPrimitives.ReadUInt64BigEndian(prefix[(timescaleOffset + 4)..]); + + if (track.MediaTimescale == 0) + { + throw new InvalidImageContentException("The HEIF image-sequence media header has a zero time scale."); + } + } + + /// + /// Parses a media handler and returns its four-character handler type. + /// + /// The stream positioned at the handler payload. + /// The validated handler payload length. + /// The parser-owned reusable scratch span. + /// The declared handler type. + private static Heif4CharCode ParseHandler(Stream stream, long boxLength, Span scratch) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 24, "handler"); + EnsureVersionAndFlags(prefix, 0, 0, "handler"); + if (BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]) != 0) + { + throw new InvalidImageContentException("The HEIF image-sequence handler has a nonzero predefined field."); + } + + return (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(prefix[8..]); + } + + /// + /// Locates the self-contained data reference and sample table inside a selected track. + /// + /// The stream positioned at the media-information payload. + /// The validated media-information payload length. + /// The selected track receiving its sample table. + /// The parser-owned reusable scratch span. + private void ParseMediaInformation(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + long informationEnd = checked(stream.Position + boxLength); + BoxReference dataInformation = default; + BoxReference sampleTable = default; + while (stream.Position < informationEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, informationEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + if (childType == Heif4CharCode.Dinf) + { + SetUnique(ref dataInformation, childStart, childLength, "media information", childType); + } + else if (childType == Heif4CharCode.Stbl) + { + SetUnique(ref sampleTable, childStart, childLength, "media information", childType); + } + + stream.Position = checked(childStart + childLength); + } + + if (!dataInformation.IsPresent || !sampleTable.IsPresent) + { + throw new InvalidImageContentException("A selected HEIF image-sequence track has no data reference or sample table."); + } + + stream.Position = dataInformation.Offset; + ParseDataInformation(stream, dataInformation.Length, scratch); + stream.Position = sampleTable.Offset; + this.ParseSampleTable(stream, sampleTable.Length, track, scratch); + } + + /// + /// Requires a selected image track to address sample bytes in the current HEIF file. + /// + /// The stream positioned at the data-information payload. + /// The validated data-information payload length. + /// The parser-owned reusable scratch span. + private static void ParseDataInformation(Stream stream, long boxLength, Span scratch) + { + long informationEnd = checked(stream.Position + boxLength); + BoxReference dataReference = default; + while (stream.Position < informationEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, informationEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + if (childType == Heif4CharCode.Dref) + { + SetUnique(ref dataReference, childStart, childLength, "data information", childType); + } + + stream.Position = checked(childStart + childLength); + } + + if (!dataReference.IsPresent) + { + throw new InvalidImageContentException("A selected HEIF image-sequence track has no data-reference box."); + } + + stream.Position = dataReference.Offset; + long referenceEnd = checked(stream.Position + dataReference.Length); + ReadOnlySpan prefix = ReadPrefix(stream, dataReference.Length, scratch, 8, "data reference"); + EnsureVersionAndFlags(prefix, 0, 0, "data reference"); + if (BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]) != 1) + { + throw new InvalidImageContentException("A HEIF image-sequence track must contain exactly one data reference."); + } + + long locationLength = HeifBoxReader.ReadHeader(stream, referenceEnd, scratch, out Heif4CharCode locationType); + if (locationType != Heif4CharCode.Url || locationLength != 4) + { + throw new InvalidImageContentException("A HEIF image-sequence track uses an external data reference."); + } + + prefix = ReadPrefix(stream, locationLength, scratch, 4, "data location"); + EnsureVersionAndFlags(prefix, 0, 1, "data location"); + if (stream.Position != referenceEnd) + { + throw new InvalidImageContentException("The data-reference box contains undeclared entries."); + } + } + + /// + /// Indexes and resolves the bounded sample-table boxes required by an image sequence. + /// + /// The stream positioned at the sample-table payload. + /// The validated sample-table payload length. + /// The selected track receiving sample descriptors. + /// The parser-owned reusable scratch span. + private void ParseSampleTable(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + long tableEnd = checked(stream.Position + boxLength); + BoxReference sampleDescription = default; + BoxReference sampleTiming = default; + BoxReference sampleToChunk = default; + BoxReference sampleSizes = default; + BoxReference chunkOffsets = default; + BoxReference syncSamples = default; + + while (stream.Position < tableEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, tableEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + switch (childType) + { + case Heif4CharCode.Stsd: + SetUnique(ref sampleDescription, childStart, childLength, "sample table", childType); + break; + case Heif4CharCode.Stts: + SetUnique(ref sampleTiming, childStart, childLength, "sample table", childType); + break; + case Heif4CharCode.Stsc: + SetUnique(ref sampleToChunk, childStart, childLength, "sample table", childType); + break; + case Heif4CharCode.Stsz: + case Heif4CharCode.Stz2: + SetUnique(ref sampleSizes, childStart, childLength, "sample table", childType); + sampleSizes.Type = childType; + break; + case Heif4CharCode.Stco: + case Heif4CharCode.Co64: + SetUnique(ref chunkOffsets, childStart, childLength, "sample table", childType); + chunkOffsets.Type = childType; + break; + case Heif4CharCode.Stss: + SetUnique(ref syncSamples, childStart, childLength, "sample table", childType); + break; + case Heif4CharCode.Ctts: + case Heif4CharCode.Cslg: + throw new InvalidImageContentException( + "Composition-offset image sequences are not yet supported by the bounded HEIF presentation model."); + } + + stream.Position = checked(childStart + childLength); + } + + if (!sampleDescription.IsPresent + || !sampleTiming.IsPresent + || !sampleToChunk.IsPresent + || !sampleSizes.IsPresent + || !chunkOffsets.IsPresent) + { + throw new InvalidImageContentException("A selected HEIF image-sequence sample table is incomplete."); + } + + stream.Position = sampleDescription.Offset; + this.ParseSampleDescription(stream, sampleDescription.Length, track, scratch); + stream.Position = sampleSizes.Offset; + this.ParseSampleSizes(stream, sampleSizes.Length, sampleSizes.Type, track, scratch); + stream.Position = sampleTiming.Offset; + ulong decodedDuration = ParseSampleTiming(stream, sampleTiming.Length, track, scratch); + + if (track.MediaDuration != 0 && track.MediaDuration != ulong.MaxValue && track.MediaDuration != decodedDuration) + { + throw new InvalidImageContentException("The image-sequence sample durations do not match the media-header duration."); + } + + stream.Position = chunkOffsets.Offset; + uint chunkCount = ReadChunkCount(stream, chunkOffsets.Length, chunkOffsets.Type, scratch); + stream.Position = sampleToChunk.Offset; + using IMemoryOwner entries = this.ParseSampleToChunk( + stream, + sampleToChunk.Length, + chunkCount, + track, + scratch, + out int entryCount); + + stream.Position = chunkOffsets.Offset; + ResolveSampleLocations(stream, chunkOffsets.Length, chunkOffsets.Type, chunkCount, entries.GetSpan()[..entryCount], track, scratch); + if (syncSamples.IsPresent) + { + stream.Position = syncSamples.Offset; + ParseSyncSamples(stream, syncSamples.Length, track, scratch); + } + else + { + Span samples = track.Samples; + for (int i = 0; i < samples.Length; i++) + { + samples[i].IsSync = true; + } + } + } + + /// + /// Parses the single visual sample entry and its codec and coding-constraint children. + /// + /// The stream positioned at the sample-description payload. + /// The validated sample-description payload length. + /// The selected track receiving its codec configuration. + /// The parser-owned reusable scratch span. + private void ParseSampleDescription(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + long descriptionEnd = checked(stream.Position + boxLength); + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 8, "sample description"); + byte version = prefix[0]; + if (version is not 0 and not 1 || ReadFlags(prefix) != 0) + { + throw new InvalidImageContentException("The sample-description box has an unsupported version or flags."); + } + + if (BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]) != 1) + { + throw new InvalidImageContentException("A HEIF image-sequence track must contain exactly one sample description."); + } + + long entryLength = HeifBoxReader.ReadHeader(stream, descriptionEnd, scratch, out Heif4CharCode entryType); + if (entryType is not Heif4CharCode.Av01 and not Heif4CharCode.Hvc1 || entryLength < 78) + { + throw new InvalidImageContentException($"The image-sequence sample entry '{entryType}' is unsupported or truncated."); + } + + long entryEnd = checked(stream.Position + entryLength); + prefix = ReadPrefix(stream, entryLength, scratch, 78, "visual sample entry"); + if (BinaryPrimitives.ReadUInt16BigEndian(prefix[6..]) != 1) + { + throw new InvalidImageContentException("The image-sequence sample entry uses a nonlocal data reference."); + } + + int codedWidth = BinaryPrimitives.ReadUInt16BigEndian(prefix[24..]); + int codedHeight = BinaryPrimitives.ReadUInt16BigEndian(prefix[26..]); + if (codedWidth == 0 || codedHeight == 0) + { + throw new InvalidImageContentException("The image-sequence sample entry has zero coded dimensions."); + } + + track.CodecType = entryType; + track.CodedWidth = codedWidth; + track.CodedHeight = codedHeight; + bool configurationSeen = false; + bool codingConstraintsSeen = false; + bool auxiliaryTypeSeen = false; + while (stream.Position < entryEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, entryEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + switch (childType) + { + case Heif4CharCode.Av1C when entryType == Heif4CharCode.Av01: + if (configurationSeen) + { + throw new InvalidImageContentException("The AV1 image-sequence sample entry has duplicate codec configurations."); + } + + using (IMemoryOwner configuration = this.boxReader.ReadPayload(stream, childLength)) + { + track.Av1CodecConfiguration = new Av1CodecConfiguration(configuration.GetSpan()); + } + + configurationSeen = true; + break; + case Heif4CharCode.HvcC when entryType == Heif4CharCode.Hvc1: + if (configurationSeen) + { + throw new InvalidImageContentException("The HEVC image-sequence sample entry has duplicate codec configurations."); + } + + using (IMemoryOwner configuration = this.boxReader.ReadPayload(stream, childLength)) + { + track.HevcCodecConfiguration = new HevcCodecConfiguration(configuration.GetSpan()); + } + + configurationSeen = true; + break; + case Heif4CharCode.Ccst: + if (codingConstraintsSeen) + { + throw new InvalidImageContentException("The image-sequence sample entry has duplicate coding constraints."); + } + + ParseCodingConstraints(stream, childLength, track, scratch); + codingConstraintsSeen = true; + break; + case Heif4CharCode.Auxi: + if (auxiliaryTypeSeen) + { + throw new InvalidImageContentException("The image-sequence sample entry has duplicate auxiliary types."); + } + + track.IsAlpha = this.ParseAuxiliaryType(stream, childLength); + auxiliaryTypeSeen = true; + break; + } + + stream.Position = checked(childStart + childLength); + } + + if (stream.Position != descriptionEnd || !configurationSeen || !codingConstraintsSeen) + { + throw new InvalidImageContentException("The image-sequence sample description is incomplete or has trailing entries."); + } + } + + /// + /// Parses coding constraints that bound inter-picture references for an image sequence. + /// + /// The stream positioned at the coding-constraints payload. + /// The validated coding-constraints payload length. + /// The selected track receiving coding constraints. + /// The parser-owned reusable scratch span. + private static void ParseCodingConstraints(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + ReadOnlySpan data = ReadPrefix(stream, boxLength, scratch, 8, "coding constraints"); + if (boxLength != 8) + { + throw new InvalidImageContentException("The image-sequence coding-constraints box has an invalid length."); + } + + EnsureVersionAndFlags(data, 0, 0, "coding constraints"); + uint constraints = BinaryPrimitives.ReadUInt32BigEndian(data[4..]); + if ((constraints & 0x03FFFFFF) != 0) + { + throw new InvalidImageContentException("The image-sequence coding constraints contain nonzero reserved bits."); + } + + track.AllReferencePicturesIntra = (constraints & 0x80000000) != 0; + track.IntraPicturePredictionUsed = (constraints & 0x40000000) != 0; + track.MaximumReferencesPerPicture = (byte)((constraints >> 26) & 15); + } + + /// + /// Determines whether an auxiliary-track type identifies an alpha image sequence. + /// + /// The stream positioned at the auxiliary-type payload. + /// The validated auxiliary-type payload length. + /// when the payload contains either registered HEIF alpha URN. + private bool ParseAuxiliaryType(Stream stream, long boxLength) + { + if (boxLength < 5 || boxLength > int.MaxValue) + { + throw new InvalidImageContentException("The image-sequence auxiliary type is truncated or too large."); + } + + using IMemoryOwner payload = this.boxReader.ReadPayload(stream, boxLength); + ReadOnlySpan data = payload.GetSpan(); + EnsureVersionAndFlags(data, 0, 0, "auxiliary type"); + ReadOnlySpan type = data[4..]; + if (type[^1] != 0) + { + throw new InvalidImageContentException("The image-sequence auxiliary type is not null terminated."); + } + + type = type[..^1]; + return type.SequenceEqual("urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"u8) + || type.SequenceEqual("urn:mpeg:hevc:2015:auxid:1"u8); + } + + /// + /// Parses either full-width or compact sample sizes into the retained descriptor array. + /// + /// The stream positioned at the sample-size payload. + /// The validated sample-size payload length. + /// The full-width or compact sample-size box type. + /// The selected track receiving retained sample lengths. + /// The parser-owned reusable scratch span. + private void ParseSampleSizes(Stream stream, long boxLength, Heif4CharCode boxType, HeifSequenceTrack track, Span scratch) + { + if (boxType == Heif4CharCode.Stsz) + { + this.ParseFullSampleSizes(stream, boxLength, track, scratch); + } + else + { + this.ParseCompactSampleSizes(stream, boxLength, track, scratch); + } + } + + /// + /// Parses a full-width sample-size table without retaining entries beyond . + /// + /// The stream positioned at the sample-size payload. + /// The validated sample-size payload length. + /// The selected track receiving retained sample lengths. + /// The parser-owned reusable scratch span. + private void ParseFullSampleSizes(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 12, "sample sizes"); + EnsureVersionAndFlags(prefix, 0, 0, "sample sizes"); + uint constantSize = BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]); + uint sampleCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[8..]); + if (sampleCount == 0) + { + throw new InvalidImageContentException("The image-sequence sample-size table is empty."); + } + + long entryBytes = constantSize == 0 ? checked((long)sampleCount * 4) : 0; + if (boxLength != 12 + entryBytes) + { + throw new InvalidImageContentException("The image-sequence sample-size table length does not match its entry count."); + } + + int retainedCount = (int)Math.Min(sampleCount, (uint)this.maxFrames); + track.TotalSampleCount = sampleCount; + track.Samples = new HeifSequenceSample[retainedCount]; + if (constantSize != 0) + { + int size = ValidateSampleSize(constantSize); + Span samples = track.Samples; + for (int i = 0; i < samples.Length; i++) + { + samples[i].Length = size; + } + + return; + } + + TableReader reader = new(stream, entryBytes, scratch, "sample sizes"); + for (uint i = 0; i < sampleCount; i++) + { + int size = ValidateSampleSize(reader.ReadUInt32()); + if (i < retainedCount) + { + track.Samples[(int)i].Length = size; + } + } + } + + /// + /// Parses a compact sample-size table without expanding entries beyond . + /// + /// The stream positioned at the compact sample-size payload. + /// The validated compact sample-size payload length. + /// The selected track receiving retained sample lengths. + /// The parser-owned reusable scratch span. + private void ParseCompactSampleSizes(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 12, "compact sample sizes"); + EnsureVersionAndFlags(prefix, 0, 0, "compact sample sizes"); + if (prefix[4] != 0 || prefix[5] != 0 || prefix[6] != 0 || prefix[7] is not 4 and not 8 and not 16) + { + throw new InvalidImageContentException("The compact sample-size table has invalid reserved fields or field width."); + } + + int fieldSize = prefix[7]; + uint sampleCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[8..]); + if (sampleCount == 0) + { + throw new InvalidImageContentException("The compact sample-size table is empty."); + } + + long entryBytes = checked((((long)sampleCount * fieldSize) + 7) / 8); + if (boxLength != 12 + entryBytes) + { + throw new InvalidImageContentException("The compact sample-size table length does not match its entry count."); + } + + int retainedCount = (int)Math.Min(sampleCount, (uint)this.maxFrames); + track.TotalSampleCount = sampleCount; + track.Samples = new HeifSequenceSample[retainedCount]; + TableReader reader = new(stream, entryBytes, scratch, "compact sample sizes"); + for (uint i = 0; i < sampleCount; i++) + { + uint size; + if (fieldSize == 4) + { + byte packed = reader.ReadByte(); + size = (uint)(packed >> 4); + if (i < retainedCount) + { + track.Samples[(int)i].Length = ValidateSampleSize(size); + } + + i++; + if (i >= sampleCount) + { + if ((packed & 15) != 0) + { + throw new InvalidImageContentException("The compact sample-size table has nonzero padding bits."); + } + + break; + } + + size = (uint)(packed & 15); + } + else + { + size = fieldSize == 8 ? reader.ReadByte() : reader.ReadUInt16(); + } + + int validatedSize = ValidateSampleSize(size); + if (i < retainedCount) + { + track.Samples[(int)i].Length = validatedSize; + } + } + } + + /// + /// Expands retained sample durations and validates the complete timing run table. + /// + /// The stream positioned at the time-to-sample payload. + /// The validated time-to-sample payload length. + /// The selected track receiving retained durations. + /// The parser-owned reusable scratch span. + /// The total decoded duration in media-time-scale units. + private static ulong ParseSampleTiming(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 8, "sample timing"); + EnsureVersionAndFlags(prefix, 0, 0, "sample timing"); + uint entryCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]); + long entryBytes = checked((long)entryCount * 8); + if (entryCount == 0 || boxLength != 8 + entryBytes) + { + throw new InvalidImageContentException("The image-sequence timing table is empty or has an invalid length."); + } + + TableReader reader = new(stream, entryBytes, scratch, "sample timing"); + ulong describedSamples = 0; + ulong decodedDuration = 0; + int retainedOffset = 0; + for (uint entry = 0; entry < entryCount; entry++) + { + uint sampleCount = reader.ReadUInt32(); + uint sampleDelta = reader.ReadUInt32(); + if (sampleCount == 0 || sampleDelta == 0) + { + throw new InvalidImageContentException("The image-sequence timing table contains a zero run or duration."); + } + + describedSamples = checked(describedSamples + sampleCount); + decodedDuration = checked(decodedDuration + ((ulong)sampleCount * sampleDelta)); + int retainedRun = Math.Min((int)Math.Min(sampleCount, int.MaxValue), track.Samples.Length - retainedOffset); + Span samples = track.Samples; + for (int i = 0; i < retainedRun; i++) + { + samples[retainedOffset + i].Duration = sampleDelta; + } + + retainedOffset += retainedRun; + } + + if (describedSamples != track.TotalSampleCount) + { + throw new InvalidImageContentException("The image-sequence timing table does not describe every sample."); + } + + return decodedDuration; + } + + /// + /// Reads and validates the declared chunk count without retaining chunk offsets. + /// + /// The stream positioned at a chunk-offset payload. + /// The validated chunk-offset payload length. + /// The 32-bit or 64-bit chunk-offset box type. + /// The parser-owned reusable scratch span. + /// The nonzero number of chunks. + private static uint ReadChunkCount(Stream stream, long boxLength, Heif4CharCode boxType, Span scratch) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 8, "chunk offsets"); + EnsureVersionAndFlags(prefix, 0, 0, "chunk offsets"); + uint chunkCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]); + int entrySize = boxType == Heif4CharCode.Co64 ? 8 : 4; + if (chunkCount == 0 || boxLength != 8 + checked((long)chunkCount * entrySize)) + { + throw new InvalidImageContentException("The image-sequence chunk-offset table is empty or has an invalid length."); + } + + return chunkCount; + } + + /// + /// Parses sample-to-chunk runs into allocator-owned state bounded by the retained frame count. + /// + /// The stream positioned at the sample-to-chunk payload. + /// The validated sample-to-chunk payload length. + /// The validated number of chunks. + /// The selected track whose complete sample count is validated. + /// The parser-owned reusable scratch span. + /// Receives the number of retained mapping entries. + /// Allocator-owned sample-to-chunk runs that cover all retained samples. + private IMemoryOwner ParseSampleToChunk( + Stream stream, + long boxLength, + uint chunkCount, + HeifSequenceTrack track, + Span scratch, + out int retainedEntryCount) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 8, "sample-to-chunk"); + EnsureVersionAndFlags(prefix, 0, 0, "sample-to-chunk"); + uint entryCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]); + long entryBytes = checked((long)entryCount * 12); + if (entryCount == 0 || entryCount > chunkCount || boxLength != 8 + entryBytes) + { + throw new InvalidImageContentException("The sample-to-chunk table is empty or has an invalid length or entry count."); + } + + int retainedCapacity = (int)Math.Min(entryCount, (uint)track.Samples.Length); + IMemoryOwner owner = this.allocator.Allocate(retainedCapacity); + Span retainedEntries = owner.GetSpan(); + TableReader reader = new(stream, entryBytes, scratch, "sample-to-chunk"); + uint previousFirstChunk = 0; + uint previousSamplesPerChunk = 0; + ulong describedSamples = 0; + retainedEntryCount = 0; + + try + { + for (uint i = 0; i < entryCount; i++) + { + uint firstChunk = reader.ReadUInt32(); + uint samplesPerChunk = reader.ReadUInt32(); + uint sampleDescriptionIndex = reader.ReadUInt32(); + if ((i == 0 && firstChunk != 1) || firstChunk <= previousFirstChunk || firstChunk > chunkCount + || samplesPerChunk == 0 || sampleDescriptionIndex != 1) + { + throw new InvalidImageContentException("The sample-to-chunk table contains an invalid run."); + } + + if (i != 0) + { + describedSamples = checked(describedSamples + ((ulong)(firstChunk - previousFirstChunk) * previousSamplesPerChunk)); + } + + if (retainedEntryCount < retainedEntries.Length) + { + retainedEntries[retainedEntryCount++] = new SampleToChunkEntry(firstChunk, samplesPerChunk); + } + + previousFirstChunk = firstChunk; + previousSamplesPerChunk = samplesPerChunk; + } + + describedSamples = checked(describedSamples + ((ulong)(chunkCount + 1U - previousFirstChunk) * previousSamplesPerChunk)); + if (describedSamples != track.TotalSampleCount) + { + throw new InvalidImageContentException("The sample-to-chunk table does not map every declared sample."); + } + + return owner; + } + catch + { + owner.Dispose(); + throw; + } + } + + /// + /// Resolves retained samples directly from sequential chunk offsets and compact mapping runs. + /// + /// The stream positioned at the chunk-offset payload. + /// The validated chunk-offset payload length. + /// The 32-bit or 64-bit chunk-offset box type. + /// The validated number of chunks. + /// The retained sample-to-chunk runs. + /// The selected track receiving absolute sample locations. + /// The parser-owned reusable scratch span. + private static void ResolveSampleLocations( + Stream stream, + long boxLength, + Heif4CharCode boxType, + uint chunkCount, + ReadOnlySpan entries, + HeifSequenceTrack track, + Span scratch) + { + _ = ReadChunkCount(stream, boxLength, boxType, scratch); + int entrySize = boxType == Heif4CharCode.Co64 ? 8 : 4; + TableReader reader = new(stream, checked((long)chunkCount * entrySize), scratch, "chunk offsets"); + int retainedSample = 0; + int runIndex = 0; + for (uint chunkIndex = 0; chunkIndex < chunkCount; chunkIndex++) + { + ulong chunkOffset = entrySize == 8 ? reader.ReadUInt64() : reader.ReadUInt32(); + if (chunkOffset > (ulong)stream.Length) + { + throw new InvalidImageContentException("An image-sequence chunk offset extends beyond the file."); + } + + uint chunkNumber = chunkIndex + 1; + if (runIndex + 1 < entries.Length && entries[runIndex + 1].FirstChunk <= chunkNumber) + { + runIndex++; + } + + uint samplesPerChunk = entries[runIndex].SamplesPerChunk; + ulong sampleOffset = chunkOffset; + for (uint sampleInChunk = 0; sampleInChunk < samplesPerChunk && retainedSample < track.Samples.Length; sampleInChunk++) + { + ref HeifSequenceSample sample = ref track.Samples[retainedSample++]; + ulong sampleEnd = checked(sampleOffset + (uint)sample.Length); + if (sampleEnd > (ulong)stream.Length || sampleOffset > long.MaxValue) + { + throw new InvalidImageContentException("An image-sequence sample extends beyond the file."); + } + + sample.Offset = (long)sampleOffset; + sampleOffset = sampleEnd; + } + } + + if (retainedSample != track.Samples.Length) + { + throw new InvalidImageContentException("The image-sequence chunk table does not locate every retained sample."); + } + } + + /// + /// Applies explicit one-based sync-sample declarations to retained sample descriptors. + /// + /// The stream positioned at the sync-sample payload. + /// The validated sync-sample payload length. + /// The selected track receiving random-access markers. + /// The parser-owned reusable scratch span. + private static void ParseSyncSamples(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 8, "sync samples"); + EnsureVersionAndFlags(prefix, 0, 0, "sync samples"); + uint entryCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]); + long entryBytes = checked((long)entryCount * 4); + if (entryCount == 0 || boxLength != 8 + entryBytes) + { + throw new InvalidImageContentException("The sync-sample table is empty or has an invalid length."); + } + + TableReader reader = new(stream, entryBytes, scratch, "sync samples"); + uint previousSample = 0; + for (uint i = 0; i < entryCount; i++) + { + uint sampleNumber = reader.ReadUInt32(); + if (sampleNumber <= previousSample || sampleNumber > track.TotalSampleCount) + { + throw new InvalidImageContentException("The sync-sample table contains an invalid sample number."); + } + + if (sampleNumber <= track.Samples.Length) + { + track.Samples[(int)sampleNumber - 1].IsSync = true; + } + + previousSample = sampleNumber; + } + + if (!track.Samples[0].IsSync) + { + throw new InvalidImageContentException("The first retained image-sequence sample is not a random-access sample."); + } + } + + /// + /// Parses the single normal-rate edit list used to signal image-sequence repetition. + /// + /// The stream positioned at the edit-container payload. + /// The validated edit-container payload length. + /// The selected track receiving repetition behavior. + /// The parser-owned reusable scratch span. + private static void ParseEdit(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + long editEnd = checked(stream.Position + boxLength); + BoxReference editList = default; + while (stream.Position < editEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, editEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + if (childType == Heif4CharCode.Elst) + { + SetUnique(ref editList, childStart, childLength, "edit", childType); + } + + stream.Position = checked(childStart + childLength); + } + + if (!editList.IsPresent) + { + throw new InvalidImageContentException("The image-sequence edit container has no edit list."); + } + + stream.Position = editList.Offset; + ReadOnlySpan prefix = ReadPrefix(stream, editList.Length, scratch, 8, "edit list"); + byte version = prefix[0]; + uint flags = ReadFlags(prefix); + int entryLength = version switch + { + 0 => 12, + 1 => 20, + _ => throw new InvalidImageContentException($"The edit list has unsupported version {version}.") + }; + + if ((flags & ~1U) != 0 || BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]) != 1 || editList.Length != 8 + entryLength) + { + throw new InvalidImageContentException("The image-sequence edit list has unsupported flags, entries, or length."); + } + + prefix = ReadPrefix(stream, entryLength, scratch, entryLength, "edit-list entry"); + ulong segmentDuration; + long mediaTime; + int rateOffset; + if (version == 0) + { + segmentDuration = BinaryPrimitives.ReadUInt32BigEndian(prefix); + mediaTime = BinaryPrimitives.ReadInt32BigEndian(prefix[4..]); + rateOffset = 8; + } + else + { + segmentDuration = BinaryPrimitives.ReadUInt64BigEndian(prefix); + mediaTime = BinaryPrimitives.ReadInt64BigEndian(prefix[8..]); + rateOffset = 16; + } + + if (segmentDuration == 0 || mediaTime != 0 || BinaryPrimitives.ReadInt16BigEndian(prefix[rateOffset..]) != 1 + || BinaryPrimitives.ReadInt16BigEndian(prefix[(rateOffset + 2)..]) != 0) + { + throw new InvalidImageContentException("The image-sequence edit list requires unsupported splicing or playback rate behavior."); + } + + if ((flags & 1) == 0) + { + track.RepeatCount = 1; + } + else if (track.TrackDuration == ulong.MaxValue) + { + track.RepeatCount = 0; + } + else + { + if (track.TrackDuration == 0) + { + throw new InvalidImageContentException("A repeating image-sequence track has zero duration."); + } + + ulong plays = (track.TrackDuration / segmentDuration) + (track.TrackDuration % segmentDuration == 0 ? 0UL : 1UL); + + // The public metadata uses zero for indefinite repetition. Preserve extremely large finite edit counts as + // indefinite rather than wrapping the observable ushort play count. + track.RepeatCount = plays is 0 or > ushort.MaxValue ? (ushort)0 : (ushort)plays; + } + } + + /// + /// Validates that a linked alpha track can be matched frame-for-frame with its master color track. + /// + /// The selected master color track. + /// The optional linked alpha track. + private static void ValidateAlphaTrack(HeifSequenceTrack colorTrack, HeifSequenceTrack? alphaTrack) + { + if (alphaTrack is null) + { + return; + } + + if (alphaTrack.TotalSampleCount != colorTrack.TotalSampleCount || alphaTrack.Samples.Length != colorTrack.Samples.Length) + { + throw new InvalidImageContentException("The alpha and color image-sequence tracks contain different sample counts."); + } + + for (int i = 0; i < colorTrack.Samples.Length; i++) + { + ulong colorDuration = (ulong)colorTrack.Samples[i].Duration * alphaTrack.MediaTimescale; + ulong alphaDuration = (ulong)alphaTrack.Samples[i].Duration * colorTrack.MediaTimescale; + if (colorDuration != alphaDuration) + { + throw new InvalidImageContentException("The alpha and color image-sequence samples have different presentation durations."); + } + } + } + + /// + /// Validates and narrows a sample size to the decoder's contiguous-buffer length type. + /// + /// The file-defined unsigned sample size. + /// The positive sample size as an . + private static int ValidateSampleSize(uint size) + { + if (size == 0 || size > int.MaxValue) + { + throw new InvalidImageContentException("An image-sequence sample has an unsupported size."); + } + + return (int)size; + } + + /// + /// Records one unique child box while retaining only its stream range. + /// + /// The child reference owned by the bounded parent parser. + /// The absolute payload offset. + /// The validated payload length. + /// The parent name used in malformed-image diagnostics. + /// The unique child box type. + private static void SetUnique(ref BoxReference reference, long offset, long length, string parentName, Heif4CharCode boxType) + { + if (reference.IsPresent) + { + throw new InvalidImageContentException($"The {parentName} box contains duplicate '{boxType}' boxes."); + } + + reference = new BoxReference(offset, length, boxType); + } + + /// + /// Reads a fixed prefix from the stream's current position. + /// + /// The source stream. + /// The validated enclosing payload length. + /// The reusable destination scratch span. + /// The required prefix length. + /// The payload name used in malformed-image diagnostics. + /// The requested prefix within . + private static ReadOnlySpan ReadPrefix(Stream stream, long boxLength, Span scratch, int length, string name) + { + if (boxLength < length) + { + throw new InvalidImageContentException($"The {name} payload is truncated."); + } + + Span destination = scratch[..length]; + HeifBoxReader.ReadExactly(stream, destination, $"The {name} payload is truncated."); + return destination; + } + + /// + /// Rewinds to the start of a partially read payload and reads a larger fixed prefix. + /// + /// The source stream positioned after a four-byte prefix. + /// The validated enclosing payload length. + /// The reusable destination scratch span. + /// The required prefix length. + /// The payload name used in malformed-image diagnostics. + /// The requested prefix within . + private static ReadOnlySpan ReadPrefixFromStart(Stream stream, long boxLength, Span scratch, int length, string name) + { + stream.Position -= 4; + return ReadPrefix(stream, boxLength, scratch, length, name); + } + + /// + /// Reads the lower 24-bit flags field from a full-box prefix. + /// + /// The prefix beginning with version and flags. + /// The unsigned flags value. + private static uint ReadFlags(ReadOnlySpan data) => BinaryPrimitives.ReadUInt32BigEndian(data) & 0x00FFFFFF; + + /// + /// Requires a full-box prefix to use one supported version and flags value. + /// + /// The prefix beginning with version and flags. + /// The required version. + /// The required flags. + /// The box name used in malformed-image diagnostics. + private static void EnsureVersionAndFlags(ReadOnlySpan data, byte version, uint flags, string name) + { + if (data[0] != version || ReadFlags(data) != flags) + { + throw new InvalidImageContentException($"The {name} box has unsupported version or flags."); + } + } + + /// + /// Requires a full-box prefix to contain zero flags. + /// + /// The prefix beginning with version and flags. + /// The box name used in malformed-image diagnostics. + private static void EnsureZeroFlags(ReadOnlySpan data, string name) + { + if (ReadFlags(data) != 0) + { + throw new InvalidImageContentException($"The {name} box has unsupported flags."); + } + } + + /// + /// Identifies a selected track without retaining its codec or sample-table payloads. + /// + private struct TrackIdentity + { + /// + /// Initializes a new instance of the struct. + /// + /// The file-defined track identifier. + /// Whether the track is enabled and used in the movie presentation. + /// The displayed track width. + /// The displayed track height. + /// The track duration in movie-time-scale units. + /// The track presentation matrix. + public TrackIdentity(uint id, bool isEnabledInMovie, int width, int height, ulong trackDuration, HeifTrackMatrix matrix) + { + this.Id = id; + this.IsEnabledInMovie = isEnabledInMovie; + this.Width = width; + this.Height = height; + this.TrackDuration = trackDuration; + this.Matrix = matrix; + this.HandlerType = default; + this.AuxiliaryForTrackId = 0; + this.PremultipliedByTrackId = 0; + } + + /// + /// Gets the file-defined track identifier. + /// + public uint Id { get; } + + /// + /// Gets a value indicating whether the track is enabled and used in the movie presentation. + /// + public bool IsEnabledInMovie { get; } + + /// + /// Gets the displayed track width. + /// + public int Width { get; } + + /// + /// Gets the displayed track height. + /// + public int Height { get; } + + /// + /// Gets the track duration in movie-time-scale units. + /// + public ulong TrackDuration { get; } + + /// + /// Gets the track presentation matrix. + /// + public HeifTrackMatrix Matrix { get; } + + /// + /// Gets or sets the media handler type. + /// + public Heif4CharCode HandlerType { get; set; } + + /// + /// Gets or sets the master track served by this auxiliary track. + /// + public uint AuxiliaryForTrackId { get; set; } + + /// + /// Gets or sets the track identifier used by premultiplication signaling. + /// + public uint PremultipliedByTrackId { get; set; } + } + + /// + /// Retains one unique child payload range without creating a general box object model. + /// + private struct BoxReference + { + /// + /// Initializes a new instance of the struct. + /// + /// The absolute payload offset. + /// The validated payload length. + /// The child box type. + public BoxReference(long offset, long length, Heif4CharCode type) + { + this.Offset = offset; + this.Length = length; + this.Type = type; + this.IsPresent = true; + } + + /// + /// Gets the absolute payload offset. + /// + public long Offset { get; } + + /// + /// Gets the validated payload length. + /// + public long Length { get; } + + /// + /// Gets or sets the child box type when one logical slot accepts multiple concrete box types. + /// + public Heif4CharCode Type { get; set; } + + /// + /// Gets a value indicating whether the child was present. + /// + public bool IsPresent { get; } + } + + /// + /// Maps a one-based chunk run to its number of samples per chunk. + /// + private readonly struct SampleToChunkEntry + { + /// + /// Initializes a new instance of the struct. + /// + /// The one-based first chunk in the run. + /// The number of samples stored in each run chunk. + public SampleToChunkEntry(uint firstChunk, uint samplesPerChunk) + { + this.FirstChunk = firstChunk; + this.SamplesPerChunk = samplesPerChunk; + } + + /// + /// Gets the one-based first chunk in the run. + /// + public uint FirstChunk { get; } + + /// + /// Gets the number of samples stored in each run chunk. + /// + public uint SamplesPerChunk { get; } + } + + /// + /// Reads fixed-width sample-table values through one bounded reusable buffer. + /// + private ref struct TableReader + { + /// + /// The source stream shared by the container parser. + /// + private readonly Stream stream; + + /// + /// The parser-owned buffer reused for sequential values. + /// + private readonly Span buffer; + + /// + /// The table name used in malformed-image diagnostics. + /// + private readonly string name; + + /// + /// The number of bytes not yet loaded from the bounded table payload. + /// + private long remaining; + + /// + /// The next unread byte in . + /// + private int offset; + + /// + /// The number of valid bytes currently stored in . + /// + private int count; + + /// + /// Initializes a new instance of the struct. + /// + /// The stream positioned at the table entries. + /// The exact number of bounded entry bytes. + /// The parser-owned reusable buffer. + /// The table name used in malformed-image diagnostics. + public TableReader(Stream stream, long length, Span buffer, string name) + { + this.stream = stream; + this.buffer = buffer; + this.name = name; + this.remaining = length; + this.offset = 0; + this.count = 0; + } + + /// + /// Reads one unsigned byte from the bounded table. + /// + /// The next byte. + public byte ReadByte() + { + this.Ensure(1); + return this.buffer[this.offset++]; + } + + /// + /// Reads one big-endian unsigned 16-bit value from the bounded table. + /// + /// The next 16-bit value. + public ushort ReadUInt16() + { + this.Ensure(2); + ushort value = BinaryPrimitives.ReadUInt16BigEndian(this.buffer[this.offset..]); + this.offset += 2; + return value; + } + + /// + /// Reads one big-endian unsigned 32-bit value from the bounded table. + /// + /// The next 32-bit value. + public uint ReadUInt32() + { + this.Ensure(4); + uint value = BinaryPrimitives.ReadUInt32BigEndian(this.buffer[this.offset..]); + this.offset += 4; + return value; + } + + /// + /// Reads one big-endian unsigned 64-bit value from the bounded table. + /// + /// The next 64-bit value. + public ulong ReadUInt64() + { + this.Ensure(8); + ulong value = BinaryPrimitives.ReadUInt64BigEndian(this.buffer[this.offset..]); + this.offset += 8; + return value; + } + + /// + /// Refills the reusable buffer without reading beyond the bounded table payload. + /// + /// The number of contiguous bytes required by the next value. + private void Ensure(int required) + { + int buffered = this.count - this.offset; + if (buffered >= required) + { + return; + } + + if (buffered > 0) + { + this.buffer.Slice(this.offset, buffered).CopyTo(this.buffer); + } + + this.offset = 0; + this.count = buffered; + while (this.count < required && this.remaining > 0) + { + int requested = (int)Math.Min(this.buffer.Length - this.count, this.remaining); + int read = this.stream.Read(this.buffer.Slice(this.count, requested)); + if (read == 0) + { + throw new InvalidImageContentException($"The {this.name} table is truncated."); + } + + this.count += read; + this.remaining -= read; + } + + if (this.count < required) + { + throw new InvalidImageContentException($"The {this.name} table is truncated."); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/HeifSequenceSample.cs b/src/ImageSharp/Formats/Heif/HeifSequenceSample.cs new file mode 100644 index 000000000..a9d562263 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifSequenceSample.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Describes one retained coded sample in a HEIF image sequence. +/// +internal struct HeifSequenceSample +{ + /// + /// Gets or sets the absolute file offset of the coded sample. + /// + public long Offset { get; set; } + + /// + /// Gets or sets the coded sample length in bytes. + /// + public int Length { get; set; } + + /// + /// Gets or sets the sample duration in media-time-scale units. + /// + public uint Duration { get; set; } + + /// + /// Gets or sets a value indicating whether decoding can begin at this sample. + /// + public bool IsSync { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/HeifSequenceTrack.cs b/src/ImageSharp/Formats/Heif/HeifSequenceTrack.cs new file mode 100644 index 000000000..5659fdc62 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifSequenceTrack.cs @@ -0,0 +1,130 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Hevc; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Owns the bounded image behavior retained from one HEIF image-sequence track. +/// +internal sealed class HeifSequenceTrack +{ + /// + /// Initializes a new instance of the class. + /// + public HeifSequenceTrack() + { + } + + /// + /// Gets or sets the file-defined track identifier. + /// + public uint Id { get; set; } + + /// + /// Gets or sets the displayed track width in pixels. + /// + public int Width { get; set; } + + /// + /// Gets or sets the displayed track height in pixels. + /// + public int Height { get; set; } + + /// + /// Gets or sets the coded sample width in pixels before track presentation transforms. + /// + public int CodedWidth { get; set; } + + /// + /// Gets or sets the coded sample height in pixels before track presentation transforms. + /// + public int CodedHeight { get; set; } + + /// + /// Gets or sets the track transformation matrix. + /// + public HeifTrackMatrix Matrix { get; set; } + + /// + /// Gets or sets the media time scale in units per second. + /// + public uint MediaTimescale { get; set; } + + /// + /// Gets or sets the declared media duration in media-time-scale units. + /// + public ulong MediaDuration { get; set; } + + /// + /// Gets or sets the total number of samples declared by the sample table. + /// + public uint TotalSampleCount { get; set; } + + /// + /// Gets or sets the coded sample-entry type. + /// + public Heif4CharCode CodecType { get; set; } + + /// + /// Gets or sets the parsed AV1 configuration when is . + /// + public Av1CodecConfiguration? Av1CodecConfiguration { get; set; } + + /// + /// Gets or sets the parsed HEVC configuration when is . + /// + public HevcCodecConfiguration? HevcCodecConfiguration { get; set; } + + /// + /// Gets or sets the retained sample descriptors in decode order. + /// + public HeifSequenceSample[] Samples { get; set; } = []; + + /// + /// Gets or sets the identifier of the master track served by this auxiliary track, or zero for a master track. + /// + public uint AuxiliaryForTrackId { get; set; } + + /// + /// Gets or sets a value indicating whether the track is an alpha auxiliary image sequence. + /// + public bool IsAlpha { get; set; } + + /// + /// Gets or sets a value indicating whether the color track is premultiplied by this auxiliary alpha track. + /// + public bool IsPremultiplied { get; set; } + + /// + /// Gets or sets a value indicating whether every reference picture is intra coded. + /// + public bool AllReferencePicturesIntra { get; set; } + + /// + /// Gets or sets a value indicating whether coded pictures use intra-picture prediction. + /// + public bool IntraPicturePredictionUsed { get; set; } + + /// + /// Gets or sets the maximum number of reference pictures permitted for one coded picture. + /// + public byte MaximumReferencesPerPicture { get; set; } + + /// + /// Gets or sets the number of times the sequence is played. Zero indicates indefinite repetition. + /// + public ushort RepeatCount { get; set; } = 1; + + /// + /// Gets or sets the track handler type. + /// + public Heif4CharCode HandlerType { get; set; } + + /// + /// Gets or sets the track duration in movie-time-scale units. + /// + public ulong TrackDuration { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/HeifTrackMatrix.cs b/src/ImageSharp/Formats/Heif/HeifTrackMatrix.cs new file mode 100644 index 000000000..c6f151ffa --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifTrackMatrix.cs @@ -0,0 +1,85 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Contains the fixed-point transformation matrix of a HEIF image-sequence track. +/// +internal readonly struct HeifTrackMatrix +{ + /// + /// Initializes a new instance of the struct. + /// + /// The horizontal scale and rotation coefficient in 16.16 fixed-point form. + /// The horizontal skew and rotation coefficient in 16.16 fixed-point form. + /// The first perspective coefficient in 2.30 fixed-point form. + /// The vertical skew and rotation coefficient in 16.16 fixed-point form. + /// The vertical scale and rotation coefficient in 16.16 fixed-point form. + /// The second perspective coefficient in 2.30 fixed-point form. + /// The horizontal translation in 16.16 fixed-point form. + /// The vertical translation in 16.16 fixed-point form. + /// The homogeneous scale coefficient in 2.30 fixed-point form. + public HeifTrackMatrix(int a, int b, int u, int c, int d, int v, int x, int y, int w) + { + this.A = a; + this.B = b; + this.U = u; + this.C = c; + this.D = d; + this.V = v; + this.X = x; + this.Y = y; + this.W = w; + } + + /// + /// Gets the horizontal scale and rotation coefficient in 16.16 fixed-point form. + /// + public int A { get; } + + /// + /// Gets the horizontal skew and rotation coefficient in 16.16 fixed-point form. + /// + public int B { get; } + + /// + /// Gets the first perspective coefficient in 2.30 fixed-point form. + /// + public int U { get; } + + /// + /// Gets the vertical skew and rotation coefficient in 16.16 fixed-point form. + /// + public int C { get; } + + /// + /// Gets the vertical scale and rotation coefficient in 16.16 fixed-point form. + /// + public int D { get; } + + /// + /// Gets the second perspective coefficient in 2.30 fixed-point form. + /// + public int V { get; } + + /// + /// Gets the horizontal translation in 16.16 fixed-point form. + /// + public int X { get; } + + /// + /// Gets the vertical translation in 16.16 fixed-point form. + /// + public int Y { get; } + + /// + /// Gets the homogeneous scale coefficient in 2.30 fixed-point form. + /// + public int W { get; } + + /// + /// Gets a value indicating whether the matrix contains unsupported perspective projection. + /// + public bool HasPerspective => this.U != 0 || this.V != 0 || this.W != 0x40000000; +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs new file mode 100644 index 000000000..2abcb7ef2 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs @@ -0,0 +1,297 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using System.Text; +using SixLabors.ImageSharp.Formats.Heif; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif; + +[Trait("Format", "Heif")] +[ValidateDisposedMemoryAllocations] +public class HeifSequenceParserTests +{ + [Fact] + public void ParseResolvesLibavifShapedSampleTable() + { + byte[] data = CreateSequenceFile(1024); + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = new(Configuration.Default.MemoryAllocator, 2); + stream.Position = 8; + + HeifSequence sequence = parser.Parse(stream, GetMoviePayloadLength(data)); + + Assert.Equal(1000U, sequence.MovieTimescale); + Assert.Null(sequence.AlphaTrack); + Assert.Equal(1U, sequence.ColorTrack.Id); + Assert.Equal(320, sequence.ColorTrack.Width); + Assert.Equal(240, sequence.ColorTrack.Height); + Assert.Equal(Heif4CharCode.Av01, sequence.ColorTrack.CodecType); + Assert.NotNull(sequence.ColorTrack.Av1CodecConfiguration); + Assert.Equal(2U, sequence.ColorTrack.TotalSampleCount); + Assert.Equal(3, sequence.ColorTrack.RepeatCount); + Assert.False(sequence.ColorTrack.AllReferencePicturesIntra); + Assert.True(sequence.ColorTrack.IntraPicturePredictionUsed); + Assert.Equal(15, sequence.ColorTrack.MaximumReferencesPerPicture); + Assert.Collection( + sequence.ColorTrack.Samples, + sample => + { + Assert.Equal(1024, sample.Offset); + Assert.Equal(10, sample.Length); + Assert.Equal(100U, sample.Duration); + Assert.True(sample.IsSync); + }, + sample => + { + Assert.Equal(1034, sample.Offset); + Assert.Equal(12, sample.Length); + Assert.Equal(100U, sample.Duration); + Assert.False(sample.IsSync); + }); + } + + [Fact] + public void ParseRetainsOnlyConfiguredFrameCount() + { + byte[] data = CreateSequenceFile(1024); + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = new(Configuration.Default.MemoryAllocator, 1); + stream.Position = 8; + + HeifSequence sequence = parser.Parse(stream, GetMoviePayloadLength(data)); + + Assert.Equal(2U, sequence.ColorTrack.TotalSampleCount); + HeifSequenceSample sample = Assert.Single(sequence.ColorTrack.Samples); + Assert.Equal(1024, sample.Offset); + Assert.Equal(10, sample.Length); + Assert.Equal(100U, sample.Duration); + } + + [Fact] + public void ParseRejectsRetainedSampleBeyondFile() + { + byte[] data = CreateSequenceFile(2040); + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = new(Configuration.Default.MemoryAllocator, 2); + stream.Position = 8; + + Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data))); + } + + private static byte[] CreateSequenceFile(uint chunkOffset) + { + using MemoryStream stream = new(); + using BinaryWriter writer = new(stream, Encoding.UTF8, true); + long movie = BeginBox(writer, Heif4CharCode.Moov); + + long movieHeader = BeginBox(writer, Heif4CharCode.Mvhd); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, 1000); + WriteUInt32(writer, 600); + WriteZeros(writer, 80); + EndBox(writer, movieHeader); + + long track = BeginBox(writer, Heif4CharCode.Trak); + WriteTrackHeader(writer); + WriteEditList(writer); + + long media = BeginBox(writer, Heif4CharCode.Mdia); + WriteMediaHeader(writer); + WriteHandler(writer, Heif4CharCode.Pict); + + long mediaInformation = BeginBox(writer, Heif4CharCode.Minf); + WriteDataInformation(writer); + WriteSampleTable(writer, chunkOffset); + EndBox(writer, mediaInformation); + EndBox(writer, media); + EndBox(writer, track); + EndBox(writer, movie); + + byte[] movieBytes = stream.ToArray(); + byte[] file = new byte[2048]; + + movieBytes.CopyTo(file, 0); + return file; + } + + private static void WriteTrackHeader(BinaryWriter writer) + { + long trackHeader = BeginBox(writer, Heif4CharCode.Tkhd); + WriteFullBoxHeader(writer, 0, 3); + WriteUInt32(writer, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, 1); + WriteUInt32(writer, 0); + WriteUInt32(writer, 600); + WriteZeros(writer, 16); + WriteUInt32(writer, 0x00010000); + WriteUInt32(writer, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, 0x00010000); + WriteUInt32(writer, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, 0x40000000); + WriteUInt32(writer, 320U << 16); + WriteUInt32(writer, 240U << 16); + EndBox(writer, trackHeader); + } + + private static void WriteEditList(BinaryWriter writer) + { + long edit = BeginBox(writer, Heif4CharCode.Edts); + long editList = BeginBox(writer, Heif4CharCode.Elst); + WriteFullBoxHeader(writer, 0, 1); + WriteUInt32(writer, 1); + WriteUInt32(writer, 200); + WriteUInt32(writer, 0); + WriteUInt16(writer, 1); + WriteUInt16(writer, 0); + EndBox(writer, editList); + EndBox(writer, edit); + } + + private static void WriteMediaHeader(BinaryWriter writer) + { + long mediaHeader = BeginBox(writer, Heif4CharCode.Mdhd); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, 1000); + WriteUInt32(writer, 200); + WriteUInt16(writer, 21956); + WriteUInt16(writer, 0); + EndBox(writer, mediaHeader); + } + + private static void WriteHandler(BinaryWriter writer, Heif4CharCode handlerType) + { + long handler = BeginBox(writer, Heif4CharCode.Hdlr); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, (uint)handlerType); + WriteZeros(writer, 12); + writer.Write((byte)0); + EndBox(writer, handler); + } + + private static void WriteDataInformation(BinaryWriter writer) + { + long dataInformation = BeginBox(writer, Heif4CharCode.Dinf); + long dataReference = BeginBox(writer, Heif4CharCode.Dref); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, 1); + long location = BeginBox(writer, Heif4CharCode.Url); + WriteFullBoxHeader(writer, 0, 1); + EndBox(writer, location); + EndBox(writer, dataReference); + EndBox(writer, dataInformation); + } + + private static void WriteSampleTable(BinaryWriter writer, uint chunkOffset) + { + long sampleTable = BeginBox(writer, Heif4CharCode.Stbl); + WriteSampleDescription(writer); + + long timing = BeginBox(writer, Heif4CharCode.Stts); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, 1); + WriteUInt32(writer, 2); + WriteUInt32(writer, 100); + EndBox(writer, timing); + + long sampleToChunk = BeginBox(writer, Heif4CharCode.Stsc); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, 1); + WriteUInt32(writer, 1); + WriteUInt32(writer, 2); + WriteUInt32(writer, 1); + EndBox(writer, sampleToChunk); + + long sampleSizes = BeginBox(writer, Heif4CharCode.Stsz); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, 2); + WriteUInt32(writer, 10); + WriteUInt32(writer, 12); + EndBox(writer, sampleSizes); + + long chunkOffsets = BeginBox(writer, Heif4CharCode.Stco); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, 1); + WriteUInt32(writer, chunkOffset); + EndBox(writer, chunkOffsets); + + long syncSamples = BeginBox(writer, Heif4CharCode.Stss); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, 1); + WriteUInt32(writer, 1); + EndBox(writer, syncSamples); + EndBox(writer, sampleTable); + } + + private static void WriteSampleDescription(BinaryWriter writer) + { + long description = BeginBox(writer, Heif4CharCode.Stsd); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, 1); + long sampleEntry = BeginBox(writer, Heif4CharCode.Av01); + WriteZeros(writer, 6); + WriteUInt16(writer, 1); + WriteZeros(writer, 16); + WriteUInt16(writer, 320); + WriteUInt16(writer, 240); + WriteUInt32(writer, 0x00480000); + WriteUInt32(writer, 0x00480000); + WriteUInt32(writer, 0); + WriteUInt16(writer, 1); + WriteZeros(writer, 32); + WriteUInt16(writer, 0x18); + WriteUInt16(writer, ushort.MaxValue); + + long configuration = BeginBox(writer, Heif4CharCode.Av1C); + writer.Write(new byte[] { 0x81, 0, 0, 0 }); + EndBox(writer, configuration); + + long codingConstraints = BeginBox(writer, Heif4CharCode.Ccst); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, 0x7C000000); + EndBox(writer, codingConstraints); + EndBox(writer, sampleEntry); + EndBox(writer, description); + } + + private static long BeginBox(BinaryWriter writer, Heif4CharCode type) + { + long start = writer.BaseStream.Position; + WriteUInt32(writer, 0); + WriteUInt32(writer, (uint)type); + return start; + } + + private static void EndBox(BinaryWriter writer, long start) + { + long end = writer.BaseStream.Position; + writer.BaseStream.Position = start; + WriteUInt32(writer, checked((uint)(end - start))); + writer.BaseStream.Position = end; + } + + private static void WriteFullBoxHeader(BinaryWriter writer, byte version, uint flags) + => WriteUInt32(writer, ((uint)version << 24) | flags); + + private static void WriteUInt16(BinaryWriter writer, ushort value) + => writer.Write(BinaryPrimitives.ReverseEndianness(value)); + + private static void WriteUInt32(BinaryWriter writer, uint value) + => writer.Write(BinaryPrimitives.ReverseEndianness(value)); + + private static void WriteZeros(BinaryWriter writer, int count) => writer.Write(new byte[count]); + + private static int GetMoviePayloadLength(byte[] data) + => checked((int)BinaryPrimitives.ReadUInt32BigEndian(data) - 8); +}