Browse Source

Parse bounded HEIF image sequence tables

pull/2633/head
James Jackson-South 1 week ago
parent
commit
42c874b8e8
  1. 34
      HEIF_IMPLEMENTATION_PLAN.md
  2. 165
      src/ImageSharp/Formats/Heif/Heif4CharCode.cs
  3. 33
      src/ImageSharp/Formats/Heif/Heif4CharCode.tt
  4. 223
      src/ImageSharp/Formats/Heif/HeifBoxReader.cs
  5. 250
      src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
  6. 38
      src/ImageSharp/Formats/Heif/HeifSequence.cs
  7. 1791
      src/ImageSharp/Formats/Heif/HeifSequenceParser.cs
  8. 30
      src/ImageSharp/Formats/Heif/HeifSequenceSample.cs
  9. 130
      src/ImageSharp/Formats/Heif/HeifSequenceTrack.cs
  10. 85
      src/ImageSharp/Formats/Heif/HeifTrackMatrix.cs
  11. 297
      tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs

34
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] 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. - [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. - `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. - [ ] 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. - [ ] 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. - [ ] 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. - [ ] 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. 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 ## Reference hierarchy
Use the references in this order when behavior differs: 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. | | `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. | | `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. | | `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.ReadBoxHeader` and `HeifDecoderCore.ParseBoxHeader` | ISO/IEC 14496-12 section 4.2.2 basic box syntax | libavif `src/stream.c` functions `avifROStreamReadBoxHeaderPartial` and `avifROStreamReadBoxHeader` at `092276ce89098ead06db80975173191e5fee1826` | Resolve 32-bit, 64-bit, UUID, and top-level size-zero boxes into content lengths only after validating the complete variable-sized header and the remaining parent boundary. Nested size-zero boxes are invalid; large skips retain 64-bit offsets. |
| `HeifDecoderCore.ParseMetadata` | ISO/IEC 14496-12 `MetaBox` and HEIF item declarations, locations, properties, and associations | libavif `src/read.c` functions `avifParseMetaBox`, `avifMetaFindOrCreateItem`, `avifParseItemLocationBox`, and `avifParseItemPropertiesBox` at `092276ce89098ead06db80975173191e5fee1826` | Index unique recognized metadata children by type and payload location, then parse them in dependency order so physical placement does not control item lookup or property association. Duplicate unique children and truncated full-box headers are invalid. | | `HeifDecoderCore.ParseMetadata` | ISO/IEC 14496-12 `MetaBox` and HEIF item declarations, locations, properties, and associations | libavif `src/read.c` functions `avifParseMetaBox`, `avifMetaFindOrCreateItem`, `avifParseItemLocationBox`, and `avifParseItemPropertiesBox` at `092276ce89098ead06db80975173191e5fee1826` | Index unique recognized metadata children by type and payload location, then parse them in dependency order so physical placement does not control item lookup or property association. Duplicate unique children and truncated full-box headers are invalid. |
| `HeifDecoderCore.ApplyAssociatedMetadata` | HEIF Annex A Exif item data, MIME metadata items, and `cdsc` item references | libavif `src/read.c` function `avifDecoderFindMetadata`, `src/exif.c` function `avifGetExifTiffHeaderOffset`, and the Exif/XMP item writing paths in `src/write.c` at `092276ce89098ead06db80975173191e5fee1826` | Resolve only metadata items whose `cdsc` reference identifies the decoded primary image, validate the Exif TIFF-header offset, and attach Exif or `application/rdf+xml` XMP through ImageSharp's existing profile types. This is a bounded still-image metadata path; it does not introduce a generic ISO BMFF metadata, media, or track model. | | `HeifDecoderCore.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. |

165
src/ImageSharp/Formats/Heif/Heif4CharCode.cs

@ -28,6 +28,136 @@ public enum Heif4CharCode : uint
/// </summary> /// </summary>
Mdat = 0x6D646174U, Mdat = 0x6D646174U,
/// <summary>
/// Movie container for an image sequence.
/// </summary>
Moov = 0x6D6F6F76U,
/// <summary>
/// Movie header for an image sequence.
/// </summary>
Mvhd = 0x6D766864U,
/// <summary>
/// Image sequence track.
/// </summary>
Trak = 0x7472616BU,
/// <summary>
/// Image sequence track header.
/// </summary>
Tkhd = 0x746B6864U,
/// <summary>
/// Image sequence track references.
/// </summary>
Tref = 0x74726566U,
/// <summary>
/// Image sequence edit container.
/// </summary>
Edts = 0x65647473U,
/// <summary>
/// Image sequence edit list.
/// </summary>
Elst = 0x656C7374U,
/// <summary>
/// Image sequence media container.
/// </summary>
Mdia = 0x6D646961U,
/// <summary>
/// Image sequence media header.
/// </summary>
Mdhd = 0x6D646864U,
/// <summary>
/// Image sequence media information.
/// </summary>
Minf = 0x6D696E66U,
/// <summary>
/// Image sequence sample table.
/// </summary>
Stbl = 0x7374626CU,
/// <summary>
/// Image sequence sample descriptions.
/// </summary>
Stsd = 0x73747364U,
/// <summary>
/// Image sequence sample timing.
/// </summary>
Stts = 0x73747473U,
/// <summary>
/// Image sequence composition offsets.
/// </summary>
Ctts = 0x63747473U,
/// <summary>
/// Image sequence composition-to-decode timing.
/// </summary>
Cslg = 0x63736C67U,
/// <summary>
/// Image sequence sample-to-chunk map.
/// </summary>
Stsc = 0x73747363U,
/// <summary>
/// Image sequence sample sizes.
/// </summary>
Stsz = 0x7374737AU,
/// <summary>
/// Compact image sequence sample sizes.
/// </summary>
Stz2 = 0x73747A32U,
/// <summary>
/// Image sequence 32-bit chunk offsets.
/// </summary>
Stco = 0x7374636FU,
/// <summary>
/// Image sequence 64-bit chunk offsets.
/// </summary>
Co64 = 0x636F3634U,
/// <summary>
/// Image sequence sync samples.
/// </summary>
Stss = 0x73747373U,
/// <summary>
/// Image sequence sample group descriptions.
/// </summary>
Sgpd = 0x73677064U,
/// <summary>
/// Image sequence sample-to-group map.
/// </summary>
Sbgp = 0x73626770U,
/// <summary>
/// Image sequence coding constraints.
/// </summary>
Ccst = 0x63637374U,
/// <summary>
/// Auxiliary image sequence type.
/// </summary>
Auxi = 0x61757869U,
/// <summary>
/// Self-contained data location.
/// </summary>
Url = 0x75726C20U,
/// <summary> /// <summary>
/// Item Information Entry. /// Item Information Entry.
/// </summary> /// </summary>
@ -218,11 +348,36 @@ public enum Heif4CharCode : uint
/// </summary> /// </summary>
Avis = 0x61766973U, Avis = 0x61766973U,
/// <summary>
/// AVIF intra-only image sequence brand.
/// </summary>
Avio = 0x6176696FU,
/// <summary>
/// HEIF image sequence structural brand.
/// </summary>
Msf1 = 0x6D736631U,
/// <summary>
/// ISO base media version 8 structural brand.
/// </summary>
Iso8 = 0x69736F38U,
/// <summary> /// <summary>
/// HEVC-coded image item. /// HEVC-coded image item.
/// </summary> /// </summary>
Hvc1 = 0x68766331U, Hvc1 = 0x68766331U,
/// <summary>
/// Layered High Efficiency Coding sample.
/// </summary>
Hvc2 = 0x68766332U,
/// <summary>
/// Layered High Efficiency Coding sample.
/// </summary>
Lhv1 = 0x6C687631U,
/// <summary> /// <summary>
/// Legacy JPEG coded tile. /// Legacy JPEG coded tile.
/// </summary> /// </summary>
@ -318,6 +473,16 @@ public enum Heif4CharCode : uint
/// </summary> /// </summary>
Pict = 0x70696374U, Pict = 0x70696374U,
/// <summary>
/// Auxiliary image sequence handler type.
/// </summary>
Auxv = 0x61757876U,
/// <summary>
/// Video handler type.
/// </summary>
Vide = 0x76696465U,
/// <summary> /// <summary>
/// Unique Identifier. /// Unique Identifier.
/// </summary> /// </summary>

33
src/ImageSharp/Formats/Heif/Heif4CharCode.tt

@ -10,6 +10,32 @@
"ftyp", "File Type", "ftyp", "File Type",
"meta", "Metadata", "meta", "Metadata",
"mdat", "Media Data", "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", "infe", "Item Information Entry",
"idat", "Item Data", "idat", "Item Data",
"iloc", "Item Location", "iloc", "Item Location",
@ -48,7 +74,12 @@
"mif1", "High Efficient File brand", "mif1", "High Efficient File brand",
"avif", "AVIF brand", "avif", "AVIF brand",
"avis", "AVIF image sequence 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", "hvc1", "High Efficiency Coding tile",
"hvc2", "Layered High Efficiency Coding sample",
"lhv1", "Layered High Efficiency Coding sample",
"jpeg", "Legacy JPEG coded tile", "jpeg", "Legacy JPEG coded tile",
"jpgs", "JPEG image sequence brand", "jpgs", "JPEG image sequence brand",
"av01", "AOMedia Video Coding tile", "av01", "AOMedia Video Coding tile",
@ -68,6 +99,8 @@
"mime", "MIME type", "mime", "MIME type",
"uri ", "URI", "uri ", "URI",
"pict", "Picture handler type", "pict", "Picture handler type",
"auxv", "Auxiliary image sequence handler type",
"vide", "Video handler type",
"uuid", "Unique Identifier", "uuid", "Unique Identifier",
"free", "Free space", "free", "Free space",
"nclx", "CICP color information", "nclx", "CICP color information",

223
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;
/// <summary>
/// Reads bounded ISO BMFF box headers and payloads used by the HEIF image container.
/// </summary>
internal readonly struct HeifBoxReader
{
/// <summary>
/// The allocator used for payloads that must be materialized while parsing.
/// </summary>
private readonly MemoryAllocator allocator;
/// <summary>
/// Initializes a new instance of the <see cref="HeifBoxReader"/> struct.
/// </summary>
/// <param name="allocator">The allocator used for bounded payload buffers.</param>
public HeifBoxReader(MemoryAllocator allocator) => this.allocator = allocator;
/// <summary>
/// Reads an ISO BMFF box header using caller-owned scratch and resolves its validated payload length.
/// </summary>
/// <param name="stream">The stream positioned at the box size field.</param>
/// <param name="parentEndPosition">The absolute end position of the containing box or file.</param>
/// <param name="scratch">Caller-owned scratch containing at least eight bytes.</param>
/// <param name="boxType">Receives the box four-character code.</param>
/// <param name="topLevel">Indicates whether a size-zero box may extend to the end of the file.</param>
/// <returns>The number of payload bytes following the complete variable-length header.</returns>
public static long ReadHeader(Stream stream, long parentEndPosition, Span<byte> scratch, out Heif4CharCode boxType, bool topLevel = false)
{
if (parentEndPosition - stream.Position < 8)
{
throw new InvalidImageContentException("Not enough data to read the box header.");
}
Span<byte> 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;
}
/// <summary>
/// Parses an ISO BMFF child-box header from a bounded parent payload.
/// </summary>
/// <param name="buffer">The remaining bytes in the parent payload, beginning at the child size field.</param>
/// <param name="length">Receives the validated child payload length.</param>
/// <param name="boxType">Receives the child box four-character code.</param>
/// <returns>The number of bytes occupied by the complete child header.</returns>
public static int ParseHeader(ReadOnlySpan<byte> 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;
}
/// <summary>
/// Reads a complete bounded box payload into allocator-owned memory.
/// </summary>
/// <param name="stream">The stream positioned at the payload start.</param>
/// <param name="length">The validated payload length.</param>
/// <returns>An owner containing exactly the requested payload bytes.</returns>
public IMemoryOwner<byte> 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<byte> memory = this.allocator.Allocate<byte>(bufferLength);
try
{
ReadExactly(stream, memory.GetSpan(), "Stream length is not sufficient for box content.");
return memory;
}
catch
{
memory.Dispose();
throw;
}
}
/// <summary>
/// Advances over a validated box payload without narrowing its 64-bit length.
/// </summary>
/// <param name="stream">The seekable container stream.</param>
/// <param name="length">The validated payload length.</param>
public static void Skip(Stream stream, long length) => stream.Seek(length, SeekOrigin.Current);
/// <summary>
/// Validates a child payload length against the bytes remaining in its parent.
/// </summary>
/// <param name="length">The declared child payload length.</param>
/// <param name="parentLength">The number of bytes remaining in the parent.</param>
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.");
}
}
/// <summary>
/// Reads exactly the requested number of bytes or rejects the truncated payload.
/// </summary>
/// <param name="stream">The source stream.</param>
/// <param name="destination">The complete destination span.</param>
/// <param name="message">The malformed-image message used when the stream ends early.</param>
public static void ReadExactly(Stream stream, Span<byte> 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;
}
}
}

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

@ -54,6 +54,16 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
/// </summary> /// </summary>
private readonly ImageMetadata metadata; private readonly ImageMetadata metadata;
/// <summary>
/// The shared bounded box reader used by the item and image-sequence container paths.
/// </summary>
private readonly HeifBoxReader boxReader;
/// <summary>
/// The fixed scratch buffer reused for all item-container box headers in this decode operation.
/// </summary>
private readonly byte[] boxHeaderScratch;
/// <summary> /// <summary>
/// The item identifier selected by the primary-item box. /// The item identifier selected by the primary-item box.
/// </summary> /// </summary>
@ -88,6 +98,8 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
{ {
this.configuration = options.Configuration; this.configuration = options.Configuration;
this.metadata = new ImageMetadata(); this.metadata = new ImageMetadata();
this.boxReader = new HeifBoxReader(this.configuration.MemoryAllocator);
this.boxHeaderScratch = new byte[8];
this.items = []; this.items = [];
this.itemLinks = []; 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. // the metadata box. Complete the top-level scan before resolving and decoding the primary item.
while (stream.Position < stream.Length) 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) switch (boxType)
{ {
case Heif4CharCode.Meta: case Heif4CharCode.Meta:
@ -117,14 +129,14 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
break; break;
case Heif4CharCode.Mdat: case Heif4CharCode.Mdat:
case Heif4CharCode.Free: case Heif4CharCode.Free:
SkipBox(stream, boxLength); HeifBoxReader.Skip(stream, boxLength);
break; break;
case 0U: case 0U:
// Some files have trailing zeros, skiping to EOF. // Some files have trailing zeros, skiping to EOF.
SkipBox(stream, stream.Length - stream.Position); HeifBoxReader.Skip(stream, stream.Length - stream.Position);
break; break;
default: default:
SkipBox(stream, boxLength); HeifBoxReader.Skip(stream, boxLength);
break; break;
} }
} }
@ -149,7 +161,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
// metadata come from item declarations and associated properties rather than reconstructed pixels. // metadata come from item declarations and associated properties rather than reconstructed pixels.
while (stream.Position < stream.Length) 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) switch (boxType)
{ {
case Heif4CharCode.Meta: case Heif4CharCode.Meta:
@ -157,7 +169,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
break; break;
default: default:
// Silently skip all other box types. // Silently skip all other box types.
SkipBox(stream, boxLength); HeifBoxReader.Skip(stream, boxLength);
break; break;
} }
} }
@ -181,7 +193,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
/// <returns><see langword="true"/> when the complete file-type payload advertises a supported still-image brand.</returns> /// <returns><see langword="true"/> when the complete file-type payload advertises a supported still-image brand.</returns>
private bool CheckFileTypeBox(BufferedReadStream stream) 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) if (boxType != Heif4CharCode.Ftyp)
{ {
return false; return false;
@ -192,7 +204,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
return false; return false;
} }
using IMemoryOwner<byte> boxMemory = this.ReadIntoBuffer(stream, boxLength); using IMemoryOwner<byte> boxMemory = this.boxReader.ReadPayload(stream, boxLength);
Span<byte> boxBuffer = boxMemory.GetSpan(); Span<byte> boxBuffer = boxMemory.GetSpan();
return HeifConstants.IsSupportedFileType(boxBuffer); return HeifConstants.IsSupportedFileType(boxBuffer);
} }
@ -300,145 +312,6 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
} }
} }
/// <summary>
/// Reads an ISO BMFF box header and resolves its validated payload length.
/// </summary>
/// <param name="stream">The stream positioned at the box size field.</param>
/// <param name="parentEndPosition">The absolute end position of the containing box or file.</param>
/// <param name="boxType">Receives the box four-character code.</param>
/// <param name="topLevel">Indicates whether a size-zero box may extend to the end of the file.</param>
/// <returns>The number of payload bytes following the complete variable-length header.</returns>
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<byte> 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;
}
/// <summary>
/// Parses an ISO BMFF child-box header from a bounded parent payload.
/// </summary>
/// <param name="buffer">The remaining bytes in the parent payload, beginning at the child size field.</param>
/// <param name="length">Receives the validated child payload length.</param>
/// <param name="boxType">Receives the child box four-character code.</param>
/// <returns>The number of bytes occupied by the complete child header.</returns>
private static int ParseBoxHeader(Span<byte> 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;
}
/// <summary> /// <summary>
/// Indexes and parses the recognized children of a metadata box. /// Indexes and parses the recognized children of a metadata box.
/// </summary> /// </summary>
@ -459,7 +332,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
Dictionary<Heif4CharCode, (long Offset, long Length)> boxes = []; Dictionary<Heif4CharCode, (long Offset, long Length)> boxes = [];
while (stream.Position < endPosition) 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) if (Array.IndexOf(MetadataParseOrder, boxType) >= 0)
{ {
// Association and location boxes can precede the item declarations they reference. // 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) foreach (Heif4CharCode boxType in MetadataParseOrder)
@ -523,7 +396,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
/// <param name="boxLength">The bounded handler payload length.</param> /// <param name="boxLength">The bounded handler payload length.</param>
private void ParseHandler(BufferedReadStream stream, long boxLength) private void ParseHandler(BufferedReadStream stream, long boxLength)
{ {
using IMemoryOwner<byte> boxMemory = this.ReadIntoBuffer(stream, boxLength); using IMemoryOwner<byte> boxMemory = this.boxReader.ReadPayload(stream, boxLength);
Span<byte> boxBuffer = boxMemory.GetSpan(); Span<byte> boxBuffer = boxMemory.GetSpan();
EnsureBufferRemaining(boxBuffer, 0, 12, "handler"); EnsureBufferRemaining(boxBuffer, 0, 12, "handler");
@ -545,7 +418,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
/// <param name="boxLength">The bounded item-information payload length.</param> /// <param name="boxLength">The bounded item-information payload length.</param>
private void ParseItemInfo(BufferedReadStream stream, long boxLength) private void ParseItemInfo(BufferedReadStream stream, long boxLength)
{ {
using IMemoryOwner<byte> boxMemory = this.ReadIntoBuffer(stream, boxLength); using IMemoryOwner<byte> boxMemory = this.boxReader.ReadPayload(stream, boxLength);
Span<byte> boxBuffer = boxMemory.GetSpan(); Span<byte> boxBuffer = boxMemory.GetSpan();
EnsureBufferRemaining(boxBuffer, 0, 4, "item info"); EnsureBufferRemaining(boxBuffer, 0, 4, "item info");
@ -577,7 +450,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
/// <returns>The complete item-information-entry box length.</returns> /// <returns>The complete item-information-entry box length.</returns>
private int ParseItemInfoEntry(Span<byte> buffer) private int ParseItemInfoEntry(Span<byte> 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) if (boxType != Heif4CharCode.Infe)
{ {
throw new InvalidImageContentException($"The item info box contains unexpected child '{boxType}'."); throw new InvalidImageContentException($"The item info box contains unexpected child '{boxType}'.");
@ -699,7 +572,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
/// <param name="boxLength">The bounded item-reference payload length.</param> /// <param name="boxLength">The bounded item-reference payload length.</param>
private void ParseItemReference(BufferedReadStream stream, long boxLength) private void ParseItemReference(BufferedReadStream stream, long boxLength)
{ {
using IMemoryOwner<byte> boxMemory = this.ReadIntoBuffer(stream, boxLength); using IMemoryOwner<byte> boxMemory = this.boxReader.ReadPayload(stream, boxLength);
Span<byte> boxBuffer = boxMemory.GetSpan(); Span<byte> boxBuffer = boxMemory.GetSpan();
EnsureBufferRemaining(boxBuffer, 0, 4, "item reference"); EnsureBufferRemaining(boxBuffer, 0, 4, "item reference");
@ -714,7 +587,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
bytesRead += 4; bytesRead += 4;
while (bytesRead < boxLength) 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); int referenceEnd = checked(bytesRead + referenceHeaderLength + (int)referenceLength);
Span<byte> referenceBuffer = boxBuffer[..referenceEnd]; Span<byte> referenceBuffer = boxBuffer[..referenceEnd];
bytesRead += referenceHeaderLength; bytesRead += referenceHeaderLength;
@ -756,7 +629,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
/// <param name="boxLength">The bounded primary-item payload length.</param> /// <param name="boxLength">The bounded primary-item payload length.</param>
private void ParsePrimaryItem(BufferedReadStream stream, long boxLength) private void ParsePrimaryItem(BufferedReadStream stream, long boxLength)
{ {
using IMemoryOwner<byte> boxMemory = this.ReadIntoBuffer(stream, boxLength); using IMemoryOwner<byte> boxMemory = this.boxReader.ReadPayload(stream, boxLength);
Span<byte> boxBuffer = boxMemory.GetSpan(); Span<byte> boxBuffer = boxMemory.GetSpan();
EnsureBufferRemaining(boxBuffer, 0, 4, "primary item"); EnsureBufferRemaining(boxBuffer, 0, 4, "primary item");
@ -789,7 +662,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
List<(long Offset, long Length)> associations = []; List<(long Offset, long Length)> associations = [];
while (stream.Position < endBoxPosition) 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 (containerType == Heif4CharCode.Ipco)
{ {
if (propertyContainer.HasValue) 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. // 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) if (!propertyContainer.HasValue)
@ -835,8 +708,8 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
long endPosition = stream.Position + boxLength; long endPosition = stream.Position + boxLength;
while (stream.Position < endPosition) while (stream.Position < endPosition)
{ {
long itemLength = this.ReadBoxHeader(stream, endPosition, out Heif4CharCode itemType); long itemLength = HeifBoxReader.ReadHeader(stream, endPosition, this.boxHeaderScratch, out Heif4CharCode itemType);
using IMemoryOwner<byte> boxMemory = this.ReadIntoBuffer(stream, itemLength); using IMemoryOwner<byte> boxMemory = this.boxReader.ReadPayload(stream, itemLength);
Span<byte> boxBuffer = boxMemory.GetSpan(); Span<byte> boxBuffer = boxMemory.GetSpan();
switch (itemType) switch (itemType)
{ {
@ -1345,7 +1218,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
/// <param name="properties">The properties in the order used by association indices.</param> /// <param name="properties">The properties in the order used by association indices.</param>
private void ParsePropertyAssociation(BufferedReadStream stream, long boxLength, List<KeyValuePair<Heif4CharCode, object>> properties) private void ParsePropertyAssociation(BufferedReadStream stream, long boxLength, List<KeyValuePair<Heif4CharCode, object>> properties)
{ {
using IMemoryOwner<byte> boxMemory = this.ReadIntoBuffer(stream, boxLength); using IMemoryOwner<byte> boxMemory = this.boxReader.ReadPayload(stream, boxLength);
Span<byte> boxBuffer = boxMemory.GetSpan(); Span<byte> boxBuffer = boxMemory.GetSpan();
EnsureBufferRemaining(boxBuffer, 0, 8, "item property association"); EnsureBufferRemaining(boxBuffer, 0, 8, "item property association");
byte version = boxBuffer[0]; byte version = boxBuffer[0];
@ -1593,7 +1466,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
/// <param name="boxLength">The bounded item-location payload length.</param> /// <param name="boxLength">The bounded item-location payload length.</param>
private void ParseItemLocation(BufferedReadStream stream, long boxLength) private void ParseItemLocation(BufferedReadStream stream, long boxLength)
{ {
using IMemoryOwner<byte> boxMemory = this.ReadIntoBuffer(stream, boxLength); using IMemoryOwner<byte> boxMemory = this.boxReader.ReadPayload(stream, boxLength);
Span<byte> boxBuffer = boxMemory.GetSpan(); Span<byte> boxBuffer = boxMemory.GetSpan();
int bytesRead = 0; int bytesRead = 0;
EnsureBufferRemaining(boxBuffer, bytesRead, 6, "item location"); 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."); throw new InvalidImageContentException($"Item {item.Id} uses an unsupported location origin.");
} }
EnsureBoxInsideParent(loc.Length, sourceBytesRemaining); HeifBoxReader.EnsureInsideParent(loc.Length, sourceBytesRemaining);
stream.Position = sourceOffset; stream.Position = sourceOffset;
int extentLength = (int)loc.Length; int extentLength = (int)loc.Length;
int bytesRead = stream.Read(itemBuffer.Slice(writeOffset, extentLength)); int bytesRead = stream.Read(itemBuffer.Slice(writeOffset, extentLength));
@ -2348,59 +2221,6 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
} }
} }
/// <summary>
/// Advances over a box payload without narrowing its 64-bit length.
/// </summary>
/// <param name="stream">The seekable container stream.</param>
/// <param name="boxLength">The validated payload length.</param>
private static void SkipBox(Stream stream, long boxLength)
=> stream.Seek(boxLength, SeekOrigin.Current);
/// <summary>
/// Reads a complete bounded box payload into allocator-owned memory.
/// </summary>
/// <param name="stream">The stream positioned at the payload start.</param>
/// <param name="length">The validated payload length.</param>
/// <returns>An owner containing exactly the requested payload bytes.</returns>
private IMemoryOwner<byte> 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<byte> buffer = this.configuration.MemoryAllocator.Allocate<byte>(bufferLength);
int bytesRead = stream.Read(buffer.GetSpan());
if (bytesRead != bufferLength)
{
throw new InvalidImageContentException("Stream length is not sufficient for box content.");
}
return buffer;
}
/// <summary>
/// Validates a box payload length against the bytes remaining in the file.
/// </summary>
/// <param name="boxLength">The declared box payload length.</param>
/// <param name="stream">The stream positioned at the payload start.</param>
private static void EnsureBoxBoundary(long boxLength, Stream stream)
=> EnsureBoxInsideParent(boxLength, stream.Length - stream.Position);
/// <summary>
/// Validates a child payload length against its remaining parent payload.
/// </summary>
/// <param name="boxLength">The declared child payload length.</param>
/// <param name="parentLength">The number of bytes remaining in the parent.</param>
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.");
}
}
/// <summary> /// <summary>
/// Finds an item by its file-defined identifier. /// Finds an item by its file-defined identifier.
/// </summary> /// </summary>

38
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;
/// <summary>
/// Contains the selected color and optional alpha tracks of one HEIF image sequence.
/// </summary>
internal sealed class HeifSequence
{
/// <summary>
/// Initializes a new instance of the <see cref="HeifSequence"/> class.
/// </summary>
/// <param name="colorTrack">The selected master image-sequence track.</param>
/// <param name="alphaTrack">The linked alpha image-sequence track, when present.</param>
/// <param name="movieTimescale">The movie time scale in units per second.</param>
public HeifSequence(HeifSequenceTrack colorTrack, HeifSequenceTrack? alphaTrack, uint movieTimescale)
{
this.ColorTrack = colorTrack;
this.AlphaTrack = alphaTrack;
this.MovieTimescale = movieTimescale;
}
/// <summary>
/// Gets the selected master image-sequence track.
/// </summary>
public HeifSequenceTrack ColorTrack { get; }
/// <summary>
/// Gets the linked alpha image-sequence track, when present.
/// </summary>
public HeifSequenceTrack? AlphaTrack { get; }
/// <summary>
/// Gets the movie time scale in units per second.
/// </summary>
public uint MovieTimescale { get; }
}

1791
src/ImageSharp/Formats/Heif/HeifSequenceParser.cs

File diff suppressed because it is too large

30
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;
/// <summary>
/// Describes one retained coded sample in a HEIF image sequence.
/// </summary>
internal struct HeifSequenceSample
{
/// <summary>
/// Gets or sets the absolute file offset of the coded sample.
/// </summary>
public long Offset { get; set; }
/// <summary>
/// Gets or sets the coded sample length in bytes.
/// </summary>
public int Length { get; set; }
/// <summary>
/// Gets or sets the sample duration in media-time-scale units.
/// </summary>
public uint Duration { get; set; }
/// <summary>
/// Gets or sets a value indicating whether decoding can begin at this sample.
/// </summary>
public bool IsSync { get; set; }
}

130
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;
/// <summary>
/// Owns the bounded image behavior retained from one HEIF image-sequence track.
/// </summary>
internal sealed class HeifSequenceTrack
{
/// <summary>
/// Initializes a new instance of the <see cref="HeifSequenceTrack"/> class.
/// </summary>
public HeifSequenceTrack()
{
}
/// <summary>
/// Gets or sets the file-defined track identifier.
/// </summary>
public uint Id { get; set; }
/// <summary>
/// Gets or sets the displayed track width in pixels.
/// </summary>
public int Width { get; set; }
/// <summary>
/// Gets or sets the displayed track height in pixels.
/// </summary>
public int Height { get; set; }
/// <summary>
/// Gets or sets the coded sample width in pixels before track presentation transforms.
/// </summary>
public int CodedWidth { get; set; }
/// <summary>
/// Gets or sets the coded sample height in pixels before track presentation transforms.
/// </summary>
public int CodedHeight { get; set; }
/// <summary>
/// Gets or sets the track transformation matrix.
/// </summary>
public HeifTrackMatrix Matrix { get; set; }
/// <summary>
/// Gets or sets the media time scale in units per second.
/// </summary>
public uint MediaTimescale { get; set; }
/// <summary>
/// Gets or sets the declared media duration in media-time-scale units.
/// </summary>
public ulong MediaDuration { get; set; }
/// <summary>
/// Gets or sets the total number of samples declared by the sample table.
/// </summary>
public uint TotalSampleCount { get; set; }
/// <summary>
/// Gets or sets the coded sample-entry type.
/// </summary>
public Heif4CharCode CodecType { get; set; }
/// <summary>
/// Gets or sets the parsed AV1 configuration when <see cref="CodecType"/> is <see cref="Heif4CharCode.Av01"/>.
/// </summary>
public Av1CodecConfiguration? Av1CodecConfiguration { get; set; }
/// <summary>
/// Gets or sets the parsed HEVC configuration when <see cref="CodecType"/> is <see cref="Heif4CharCode.Hvc1"/>.
/// </summary>
public HevcCodecConfiguration? HevcCodecConfiguration { get; set; }
/// <summary>
/// Gets or sets the retained sample descriptors in decode order.
/// </summary>
public HeifSequenceSample[] Samples { get; set; } = [];
/// <summary>
/// Gets or sets the identifier of the master track served by this auxiliary track, or zero for a master track.
/// </summary>
public uint AuxiliaryForTrackId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the track is an alpha auxiliary image sequence.
/// </summary>
public bool IsAlpha { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the color track is premultiplied by this auxiliary alpha track.
/// </summary>
public bool IsPremultiplied { get; set; }
/// <summary>
/// Gets or sets a value indicating whether every reference picture is intra coded.
/// </summary>
public bool AllReferencePicturesIntra { get; set; }
/// <summary>
/// Gets or sets a value indicating whether coded pictures use intra-picture prediction.
/// </summary>
public bool IntraPicturePredictionUsed { get; set; }
/// <summary>
/// Gets or sets the maximum number of reference pictures permitted for one coded picture.
/// </summary>
public byte MaximumReferencesPerPicture { get; set; }
/// <summary>
/// Gets or sets the number of times the sequence is played. Zero indicates indefinite repetition.
/// </summary>
public ushort RepeatCount { get; set; } = 1;
/// <summary>
/// Gets or sets the track handler type.
/// </summary>
public Heif4CharCode HandlerType { get; set; }
/// <summary>
/// Gets or sets the track duration in movie-time-scale units.
/// </summary>
public ulong TrackDuration { get; set; }
}

85
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;
/// <summary>
/// Contains the fixed-point transformation matrix of a HEIF image-sequence track.
/// </summary>
internal readonly struct HeifTrackMatrix
{
/// <summary>
/// Initializes a new instance of the <see cref="HeifTrackMatrix"/> struct.
/// </summary>
/// <param name="a">The horizontal scale and rotation coefficient in 16.16 fixed-point form.</param>
/// <param name="b">The horizontal skew and rotation coefficient in 16.16 fixed-point form.</param>
/// <param name="u">The first perspective coefficient in 2.30 fixed-point form.</param>
/// <param name="c">The vertical skew and rotation coefficient in 16.16 fixed-point form.</param>
/// <param name="d">The vertical scale and rotation coefficient in 16.16 fixed-point form.</param>
/// <param name="v">The second perspective coefficient in 2.30 fixed-point form.</param>
/// <param name="x">The horizontal translation in 16.16 fixed-point form.</param>
/// <param name="y">The vertical translation in 16.16 fixed-point form.</param>
/// <param name="w">The homogeneous scale coefficient in 2.30 fixed-point form.</param>
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;
}
/// <summary>
/// Gets the horizontal scale and rotation coefficient in 16.16 fixed-point form.
/// </summary>
public int A { get; }
/// <summary>
/// Gets the horizontal skew and rotation coefficient in 16.16 fixed-point form.
/// </summary>
public int B { get; }
/// <summary>
/// Gets the first perspective coefficient in 2.30 fixed-point form.
/// </summary>
public int U { get; }
/// <summary>
/// Gets the vertical skew and rotation coefficient in 16.16 fixed-point form.
/// </summary>
public int C { get; }
/// <summary>
/// Gets the vertical scale and rotation coefficient in 16.16 fixed-point form.
/// </summary>
public int D { get; }
/// <summary>
/// Gets the second perspective coefficient in 2.30 fixed-point form.
/// </summary>
public int V { get; }
/// <summary>
/// Gets the horizontal translation in 16.16 fixed-point form.
/// </summary>
public int X { get; }
/// <summary>
/// Gets the vertical translation in 16.16 fixed-point form.
/// </summary>
public int Y { get; }
/// <summary>
/// Gets the homogeneous scale coefficient in 2.30 fixed-point form.
/// </summary>
public int W { get; }
/// <summary>
/// Gets a value indicating whether the matrix contains unsupported perspective projection.
/// </summary>
public bool HasPerspective => this.U != 0 || this.V != 0 || this.W != 0x40000000;
}

297
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<InvalidImageContentException>(() => 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);
}
Loading…
Cancel
Save