Browse Source

Decode AVIF directly into destination regions

Write still images, grid tiles, and sequence frames into their final regions,
combining presentation transforms, alpha, and ICC conversion. Add explicit
chroma upsampling selection and correct high-bit-depth sample normalization.
Remove JPEG item handling and restore shared Flip/Rotate implementations.

Update encoded versus decoded metadata assertions and reference-image output.
Preserve the existing allocator identity contract. Document native reference
provenance, accepted ICC baselines, and remaining encoder acceptance gaps.

Validation: Roslynk reports zero errors; Release/net11 builds successfully.
Visual Studio VSTest passed all 10,487 selected HEIF/AV1, ICC, Flip/Rotate,
and allocator cases. Temporary native tooling and unused Hadamard changes
are excluded from this commit.
pull/2633/head
James Jackson-South 3 weeks ago
parent
commit
4c972965c9
  1. 356
      HEIF_IMPLEMENTATION_PLAN.md
  2. 6
      src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs
  3. 256
      src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs
  4. 20
      src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs
  5. 190
      src/ImageSharp/Formats/Heif/Av1/Color/Av1YuvConverter.cs
  6. 42
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs
  7. 3
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs
  8. 167
      src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterContext.cs
  9. 26
      src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterDecoder.cs
  10. 34
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs
  11. 21
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs
  12. 30
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs
  13. 85
      src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs
  14. 74
      src/ImageSharp/Formats/Heif/Components/Alpha/HeifAlphaRowSource.cs
  15. 91
      src/ImageSharp/Formats/Heif/Components/Alpha/HeifPlanarAlphaCompositor.cs
  16. 126
      src/ImageSharp/Formats/Heif/Components/Alpha/HeifPlanarAlphaResizeWorker.cs
  17. 27
      src/ImageSharp/Formats/Heif/Components/Alpha/IHeifAlphaItemDecoder.cs
  18. 100
      src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.Alpha.cs
  19. 235
      src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.Icc.cs
  20. 56
      src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.Operator.cs
  21. 443
      src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifPlanarColorConverter.cs
  22. 396
      src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifSampleConversion.cs
  23. 44
      src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.cs
  24. 394
      src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs
  25. 10
      src/ImageSharp/Formats/Heif/Heif4CharCode.cs
  26. 2
      src/ImageSharp/Formats/Heif/Heif4CharCode.tt
  27. 25
      src/ImageSharp/Formats/Heif/HeifChromaUpsampling.cs
  28. 1
      src/ImageSharp/Formats/Heif/HeifCompressionFactory.cs
  29. 20
      src/ImageSharp/Formats/Heif/HeifCompressionMethod.cs
  30. 17
      src/ImageSharp/Formats/Heif/HeifConstants.cs
  31. 16
      src/ImageSharp/Formats/Heif/HeifDecoder.cs
  32. 607
      src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
  33. 18
      src/ImageSharp/Formats/Heif/HeifDecoderOptions.cs
  34. 14
      src/ImageSharp/Formats/Heif/HeifEncoder.cs
  35. 112
      src/ImageSharp/Formats/Heif/HeifEncoderCore.cs
  36. 33
      src/ImageSharp/Formats/Heif/HeifItemDecoderUtilities.cs
  37. 6
      src/ImageSharp/Formats/Heif/HeifMetadata.cs
  38. 113
      src/ImageSharp/Formats/Heif/HeifPixelTransform.cs
  39. 32
      src/ImageSharp/Formats/Heif/IHeifItemDecoder.cs
  40. 52
      src/ImageSharp/Formats/Heif/JpegHeifItemDecoder.cs
  41. 16
      src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs
  42. 33
      src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs
  43. 17
      tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ColorConversionBenchmarks.cs
  44. 208
      tests/ImageSharp.Benchmarks/Codecs/Heif/Av1SequenceEncoderBenchmarks.cs
  45. 118
      tests/ImageSharp.Benchmarks/Codecs/Heif/README.md
  46. 6
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs
  47. 39
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs
  48. 12
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1DeblockingFilterTests.cs
  49. 445
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs
  50. 6
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderModeInfoBufferTests.cs
  51. 14
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs
  52. 71
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1FrameBufferTests.cs
  53. 145
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs
  54. 79
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionSearchTests.cs
  55. 99
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs
  56. 8
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceFrameStoreTests.cs
  57. 18
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1RegularQuantizerTests.cs
  58. 43
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TilingTests.cs
  59. 2
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs
  60. 28
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformEstimateTests.cs
  61. 285
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs
  62. 14
      tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs
  63. 27
      tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameLifecycleTests.cs
  64. 365
      tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs
  65. 653
      tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs
  66. 4
      tests/ImageSharp.Tests/Formats/Heif/HeifMetadataTests.cs
  67. 5
      tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs
  68. 9
      tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs
  69. 4
      tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeCdefMatchesReference_Rgba32_libavif-cdef-kodim23-8b.png
  70. 4
      tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLoopRestorationMatchesReference_Rgba32_libavif-restoration-kodim23-8b.png
  71. 4
      tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSuperResolutionMatchesReference_Rgba32_libavif-superres-kodim23-8b.png
  72. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_Irvine_CA.png
  73. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_Orange4x4.png
  74. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc-alpha-exif-xmp.avif/00.png
  75. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc-alpha-exif-xmp.avif/01.png
  76. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc-alpha-exif-xmp.avif/02.png
  77. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc-alpha-exif-xmp.avif/03.png
  78. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc-alpha-exif-xmp.avif/04.png
  79. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc-audio.avif/00.png
  80. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc-audio.avif/01.png
  81. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc-audio.avif/02.png
  82. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc-audio.avif/03.png
  83. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc-audio.avif/04.png
  84. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc.avif/00.png
  85. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc.avif/01.png
  86. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc.avif/02.png
  87. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc.avif/03.png
  88. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc.avif/04.png
  89. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_ducky_romm_icc_alpha.png
  90. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_jpeg444_xnconvert.png
  91. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_libavif-cdef-kodim23-8b.png
  92. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_libavif-intrabc-abc-8b-444.png
  93. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_libavif-kodim23-8b.png
  94. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_libavif-lossless-circle-8b-444.png
  95. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_libavif-palette-draw-points-8b.png
  96. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_libavif-profile-8b-400.png
  97. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_libavif-profile-8b-420.png
  98. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_libavif-profile-8b-422.png
  99. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_libavif-profile-8b-444.png
  100. 3
      tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_libavif-progressive-draw-points-8b.png

356
HEIF_IMPLEMENTATION_PLAN.md

@ -1,5 +1,161 @@
# AVIF and AV1 implementation plan # AVIF and AV1 implementation plan
## Managed checkpoint verification: 2026-09-09
ICC interpolation selection is committed as `9aeed10da`. The region conversion, container metadata,
chroma selection, and sample-rounding changes passed final combined verification.
Earlier unresolved-checkpoint notes below describe their state at the recorded investigation date.
The first combined Release/net11 VSTest run (`managed-checkpoint-r1`) passed 10,448 of 10,487 cases.
Its 39 failures exposed test expectations and reference files that had not followed the completed refactor:
- Encoded YUV range/matrix assertions now inspect encoded metadata; decoded RGB metadata is asserted as full-range identity.
- The chroma-position test explicitly selects Bilinear and retains its expected pixels.
- Allocation-return checks again use the allocator's existing unique allocation IDs. Array hash codes collided;
they did not establish duplicate disposal. TestMemoryAllocator.cs is unchanged from HEAD.
- Three old CDEF, restoration, and super-resolution PNGs now use the corresponding existing public decoder
references. Each was independently checked against the stored avifdec 16-bit output after 8-bit PNG conversion:
all three comparisons reported zero differing pixels. These are not promoted managed outputs.
- Presentation comparisons again call DebugSave before CompareToReferenceOutput.
The final source edit passed Roslynk diagnostics with zero errors and a Release/net11 build with zero errors.
The combined rerun (`managed-checkpoint-r2`) passed all 10,487 cases in 2.1611 minutes through Visual Studio
VSTest on Release/net11. This covers HEIF/AV1, ICC, Flip/Rotate, and allocator tests. Test tolerances were not widened.
FlipProcessor and RotateProcessor now match upstream/main and are included with their migrated HEIF callers.
JPEG, PNG, ImageDecoderCore, DecoderOptions, and IccProfile also match upstream/main.
The five explicitly accepted ICC PNGs remain
managed regression baselines as documented below; they do not independently prove native parity.
The unused Hadamard primitive and its two tests remain outside this checkpoint. Temporary native benchmark
changes and integration files are also excluded. Previously committed native tooling is reserved for the
user-requested final deletion commit. Complete encoder control-flow parity and end-to-end performance remain open.
## ICC interpolation selection: 2026-09-09
ColorConversionOptions.IccInterpolationMethod now exposes Auto (default), Trilinear, and Tetrahedral.
The choice is passed through converter construction and applies to both source and target profiles.
Trilinear selects the previous three-channel calculation and four-channel multilinear interpolation;
the four-channel implementation uses local values without per-pixel allocations or stackalloc.
The Unicolour comparisons explicitly select Trilinear because both local versions 6.0.0 and
8.0.0-1-g3c888f0 use multilinear interpolation. Expected values and tolerances remain unchanged.
Release/net11 Visual Studio VSTest icc-interpolation-options-r1 passed all 63 selected cases,
including all 27 full-profile comparisons and the existing CLUT/LUT calculator tests.
This resolves the 21 Unicolour failures described below. Following explicit user approval,
the four HEIF ICC comparisons now use managed-output regression references: the singular,
grid, alpha PNGs and both sequence frame PNGs were promoted from a fresh DebugSave run.
These preserve the accepted floating-point ICC results instead of the LittleCMS-quantized results.
Release/net11 Visual Studio VSTest icc-promoted-r1 passed all four exact image comparisons.
These five PNGs are managed regression baselines, not independent native-decoder evidence.
Roslynk reported zero errors before the successful Release build. No decoder option was added;
the new setting is available through the shared ColorConversionOptions API.
Earlier investigation and verification:
ClutCalculator now uses tetrahedral interpolation for three-channel device-input tables, and linear
blending of two tetrahedral slices for four-channel tables. Lab-indexed output/linking tables retain
trilinear interpolation, selected when constructing the LUT pipeline. The Vector4 contract remains;
the unused N-channel implementation and its scratch allocations were removed. Constructor channel
checks now enforce the existing one-to-four-channel calculator capacity. No span API was added.
The selection follows local LittleCMS revision ab329ad5ce09dbb1f3547b6c126031aca606eb42:
src/cmsintrp.c:623-714,1039-1078 and src/cmsio1.c:581-624,776. Comments explain arithmetic without source citations.
The large perceptual ICC interpolation difference was reproduced independently from the embedded table bytes:
sequence frame 01, pixel (488,212), changed from RGB (150,192,163) to (0,222,51), matching the unchanged reference.
After the interpolation change, raw RGB comparisons reported maximum errors of one for the singular,
grid, and both sequence outputs; no RGB component exceeded one. Differing component counts were
3511, 3508, 2072, and 2072 respectively. The alpha fixture had 11 differing green components,
each one unit. These measurements do not establish byte-exact acceptance or alpha-plane parity.
Release/net11 VSTest icc-tetrahedral-r3: 115 cases, 90 passed, 25 failed. All 22 new mathematical
interpolation cases and eight existing CLUT cases passed. Four HEIF exact image comparisons and
21 full-profile comparisons remain failing. Full-profile tests currently use Unicolour; their expected
values and tolerances have not been changed. Further source comparison and independently justified
test updates remain required. LittleCMS's integer CLUT evaluation can quantize inside an otherwise
floating-point transform (src/cmslut.c:444-455); production precision must not be reduced to mimic this.
No reference PNGs were changed for this fix. The native source, build, and transicc utility remain outside
ImageSharp. This work is not a verified commit checkpoint or complete ICC/codec acceptance.
The ICC source at 79ecb74135ad47bac7d42692905a079839b7e105 supplies both trilinear and tetrahedral
interpolation. Matching a different interpolation choice does not establish that its original algorithm
was defective. The four-channel algorithm change also remains a separate policy choice.
Rounding investigation: sequence frame 01 pixel (420,39) is RGB (1,0,2) in ActualOutput and (2,0,2)
in ReferenceOutput. The native 16-bit pre-ICC PNG contains RGB (360,0,455). Evaluating the embedded
A2B0 table and target profile in double precision from that same input gives red 1.3962556266 in
eight-bit code units. Rounding the interpolated PCS values to 16 bits instead gives red 1.5442997191;
the unoptimized LittleCMS transicc transform independently reports 1.5443. Those values round to
the observed managed and reference values respectively. This demonstrates intermediate-quantization
error for this sample, not an incorrect final rounding rule. It does not attribute every remaining
mismatch or establish precision relative to the original unquantized YUV-to-RGB result.
The profile's mft2 reader allocates a 16-bit CLUT (LittleCMS src/cmstypes.c:2395), whose float evaluator
rounds inputs and returns integer-interpolated output normalized back to float (src/cmslut.c:83-99,445-455,565-566).
Production precision, test tolerances, and reference images were not changed for this investigation.
## Non-ICC 12-bit rounding investigation: 2026-09-09
The two 12-bit fixture files have identical SHA256
3bf9f91da471749e7df639ba7945d4d94c1c3e3968c26f3619fbbcfc92790576. They contain the same five-frame
64x64 limited-range YUV422 sequence with alpha and no ICC profile. Existing outputs for
colors-animated-12bpc-keyframes-0-2-3 were compared as raw RGB and alpha independently:
frames 0-2 are exact; frame 3 has five differing RGB components; frame 4 has four differing RGB
components and 33 differing alpha samples. Maximum error is one in each component; none exceeds one.
The RGB differences are reproduced at all nine locations by evaluating the two source arithmetic orders
on the same native-decoded YUV samples. HeifSampleConversion.ReconstructChromaRowBilinear blends sample
values before HeifColorConverter normalizes them. Libavif src/reformat.c:836-841 normalizes each sample
before weighted blending. Weights and sample positions agree. Single-precision intermediate rounding
moves values across the final half-unit boundary. Relative to a double-precision calculation with the
BT.601 coefficients, managed rounding agrees at five of these nine components and native rounding at four.
Neither path is uniformly more accurate. The inspected libavif checkout is v1.4.2-76-g66663952;
comparison with v1.4.2, used by avifdec, shows no changes to this interpolation arithmetic.
Alpha has a separate demonstrated arithmetic defect: HeifPlanarAlphaCompositor.cs:120-122 multiplies
by a single-precision reciprocal. A 12-bit alpha of 2047 should scale to 32759.49816849817. Reciprocal
multiplication produces 32759.5 and packs as 32760, while direct division produces 32759.498046875
and packs as 32759. Existing frame 4 pixel (60,3) contains those respective actual/reference alpha values.
Libavif src/alpha.c:93-97 uses direct division. The initial investigation made no production edits.
Implemented normalization during the existing SIMD sample loads, before chroma interpolation. Bilinear
reconstruction retains the four separate products in closest/horizontal/vertical/diagonal addition order,
including duplicated boundary samples. The color-model traversal consumes normalized components without
normalizing them again. Alpha uses direct division in the same loader. Existing scratch rows are reused;
no buffers, overloads, expected images, or comparison tolerances were added or changed.
Release/net11 VSTest heif-12bit-order-r2 passed both exact five-frame decoder comparisons (plus four
integration cases selected by the filename filter). The broader corpus exposed a scalar-tail luma
double-normalization introduced during this change; that was corrected before final verification.
After the final source edit, Roslynk reported zero errors, the Release/net11 build succeeded, and
heif-normalization-corpus-r2 passed all 54 public Decode corpus cases. Both 12-bit sequences now have
zero RGBA component error against their unchanged references, including alpha. DebugSave refreshed the
ActualOutput images. Temporary theory enumeration was restored to its previous setting after testing.
This verifies the decode corpus, not the separate ICC-conversion tests or complete codec acceptance.
## Container range correction: 2026-09-08
AVIF item and sequence RGB conversion now uses the container CICP range when present, falling back to the
decoded AV1 range otherwise. The conversion receives the range explicitly; reconstruction and reference-frame
color state are unchanged. Alpha conversion and encoder conversion continue to use their coded ranges.
This matches libavif src/read.c:6909-6926, which retains the container range despite a conflicting sequence header.
Irvine_CA.avif now passes the unchanged exact reference-image comparison in Release/net11 VSTest
heif-irvine-range-r1 (one test, one pass). Its ActualOutput PNG was refreshed. The other six previously failing
cases were not rerun for this change. Broader sequence/alpha regression verification remains outstanding.
## Reference regeneration: 2026-09-08
Regenerated the 138 PNG frames for the 19 previously failing decoder tests using one AVIF decoding pipeline:
libavif avifdec 1.4.2, dav1d 1.5.3, one worker, explicit 16-bit RGB PNG output, and all frame indices.
Chroma upsampling is explicitly nearest for 8-bit sources and bilinear for the two 12-bit animations.
Requesting 16-bit RGB bypasses libavif's libyuv conversion path. ImageMagick 7.1.2-31 writes the final PNGs;
Rgba32 references use 8-bit output and Rgba64 references retain 16-bit output. ImageMagick does not decode AVIF here.
For the four ICC cases only, ImageMagick/LittleCMS converts the embedded source ICC profile to the exact
CompactSrgbV4Profile bytes using perceptual intent, before final 8-bit output. These cases still have a 16-bit
integer RGB intermediate before ICC conversion; no claim of a floating-point-only reference pipeline is made.
Native tooling, intermediates, and the previous-reference backup remain outside this repository in
D:/GitHub/ynse01/av1-takeover-20260905/unified-references-20260908.
The reference images were regenerated independently; no managed decoded pixels or weakened assertions were used.
Release/net11 VSTest heif-unified-references-r1 ran only the 19 previous failures: 12 passed, 7 failed, 14.5724 seconds.
Remaining pixel mismatches still require investigation. This is not decoder byte-exact acceptance or libaom parity.
## Goal ## Goal
Complete a production-quality, fully managed AV1 codec and its bounded AVIF/HEIF image container integration for ImageSharp. The finished work must decode and encode still images and bounded image sequences, preserve source precision, use ImageSharp memory ownership, and provide SIMD-first hot paths with one behaviorally identical scalar fallback. Complete a production-quality, fully managed AV1 codec and its bounded AVIF/HEIF image container integration for ImageSharp. The finished work must decode and encode still images and bounded image sequences, preserve source precision, use ImageSharp memory ownership, and provide SIMD-first hot paths with one behaviorally identical scalar fallback.
@ -8,6 +164,127 @@ This plan is the authoritative delivery checklist. A source file, unit test, bui
Checkpoint handling: work stays in the existing checkout. Do not create or use worktrees; the user reported a crash. Checkpoint handling: work stays in the existing checkout. Do not create or use worktrees; the user reported a crash.
Commit completed, verified features regularly, with native reference material and temporary integration excluded. Commit completed, verified features regularly, with native reference material and temporary integration excluded.
Cleanup commit `e38989d63` restored three JPEG production files and the PNG encoder exactly to upstream/main.
Cleanup commit `93f48a480` restored DecoderOptions, ImageDecoderCore, IccProfile, Point, and PointTests,
included the PixelOperations formatting correction, and migrated dependent HEIF integrity, ICC serialization,
and coordinate-shift callers. Its 16-file scope excludes component-domain ICC and region conversion changes.
The restored Point implementation passed 55 focused Release/net11 tests (`point-upstream-cleanup-r1.trx`).
FlipProcessor and RotateProcessor restorations remain coupled to the decoder-region caller changes: committing
only their helper removals would break the committed HEIF decoder. The region/ICC checkpoint remains unresolved.
## Required final cleanup and deletion commit
2026-09-08 chroma-mode correction: HeifDecoder now follows the specialized-options API used by JPEG.
HeifDecoderOptions.ChromaUpsampling has three values: Auto (default), NearestNeighbor, and Bilinear.
Auto chooses nearest-neighbour for 8-bit source chroma and bilinear for higher source bit depths.
The selection reaches still items, grid children, and sequence presentation. Nearest-neighbour writes
directly into component rows without interpolation scratch; bilinear retains its existing SIMD sampling path.
Existing helper callers now pass the mode explicitly. The existing bilinear test explicitly selects Bilinear;
its expected pixels and every golden remain unchanged. Roslynk reported zero compiler errors.
Release/net11 build passed; heif-chroma-modes-r1 ran 126 tests: 107 passed, 19 exact image comparisons failed.
The explicit bilinear test passed and all 125 public decoder tests ran and regenerated their PNGs.
Remaining comparisons are unresolved; passing the bilinear component check is not end-to-end parity evidence.
Explicit-mode end-to-end references, all SIMD-tier checks, and performance verification remain outstanding.
2026-09-08 nearest-neighbour presentation change: the user selected chroma sample replication,
matching JPEG's ScaledCopyTo behavior. HeifPlanarColorConverter now selects the chroma row by
the absolute source coordinate and subsampling shift. HeifSampleConversion widens and duplicates
samples directly into the exact output row with 512/256/128-bit SIMD and a scalar tail.
Odd crop origins retain the second half of the first pair; odd right edges write only the requested pixel.
Removed vertical/horizontal weighted interpolation, its scratch rows, and full-width crop reconstruction.
No intermediate rounding, golden changes, or comparison-tolerance changes were introduced.
Release/net11 build succeeded with zero errors and 1,011 existing test-project warnings.
HeifDecoderTests run heif-nearest-decoder-r1 completed all 125 cases: 106 passed, 19 exact comparisons
failed. ActualOutput PNGs were regenerated through DebugSave. The existing ImageMagick/libavif PNG
references have unreconciled presentation conversions and do not establish libaom parity.
The old component test explicitly requiring bilinear chroma is obsolete under the user's new contract;
it has not been rewritten or used as acceptance evidence. No speed improvement has been measured.
2026-09-08 container mismatch correction: local libaom decodeframe.c:4167-4242 reads range from the
sequence header and reads chroma sample position only for non-monochrome 4:2:0. Removed the container
range-equality rejection in Av1Decoder; the coded range is retained. Av1CodecConfiguration.Validate now
compares chroma sample position only when that field exists in the coded color configuration. Other
configuration comparisons and structural validation remain. XnConvert's av1C bytes 81 20 02 00 declare
4:4:4 with a sample-position value of 2; that unused value caused its former rejection. Both original
fixtures' extracted AV1 payloads decoded successfully with local aomdec. Final Release/net11 build passed
with zero warnings/errors; heif-decoder-container-position-r4 ran all 125 HeifDecoderTests: 108 passed,
17 exact pixel comparisons failed, no decoder rejection failures. XnConvert passes its exact comparison;
Irvine now decodes and saves PNG output but still differs from its reference. Reference pixels and test
assertions were not changed. These two corrections do not establish that all container checks match the
reference, nor do they resolve the remaining color-conversion comparisons.
2026-09-08 reference-output checkpoint: all 54 corpus cases in HeifDecoderTests.Decode now have named PNG
references, including every frame of both 12-bit animations. The missing 19 high-depth still references
were produced with official libavif 1.4.2 avifdec/dav1d 1.5.3 as 16-bit PNGs. Four ICC conversion cases
use native 16-bit RGB PNG intermediates with embedded source ICC profiles, then ImageMagick 7.1.2-31
LittleCMS conversion to the same CompactSrgbV4 target profile. Preservation/compaction/metadata-skip
references reuse the independent unconverted reference pixels. Native binaries/intermediates stay outside
the repository. Existing mismatching references were not replaced and exact assertions remain unchanged.
Release/net11 full HeifDecoderTests run heif-decoder-all-references-r2 completed: 125 tests, 107 passed,
18 failed, no missing-reference failures. Remaining failures require source-led investigation; these PNGs
do not establish codec completeness. ActualOutput contains DebugSave/DebugSaveMultiFrame decoder images.
The temporary runner no longer overrides test parallelism and does not stop on failure. Only net11 runs;
the user's serialization constraint concerns simultaneous target-framework runs, not individual tests.
Test-convention correction is active: decoder verification must use independently produced reference images,
and encoder verification must use TestImageProvider/VerifyEncoder with the registered independent decoder.
Calculated expected-pixel code is not a replacement for reviewable decoder reference images.
Remove ad hoc raw-output generation and its generated files, preserving meaningful assertions.
Commit 7b5e7d4dc registers ImageMagick for AVIF reference decoding through the existing test infrastructure.
Exact VerifyEncoder checks passed for 8-bit RGBA and 10/12-bit RGBA64. The reference adapter retains native
precision and normalizes ImageMagick's left-aligned 10/12-bit samples to the Rgba64 range. No tolerance changed.
The migrated test methods remain uncommitted with their decoder-refactor dependencies. The 8-bit end-to-end
encoder test now uses the original Ducky, Bike, and Splash PNG fixtures, with exact ImageMagick verification
and DebugSave PNG output. Raw-output writers have been removed from the HEIF/AV1 test sources. The legacy
ActualOutput/Heif directory was deleted; HeifEncoderTests and HeifDecoderTests were emptied before the requested
Release/net11 serialized codec rerun. The calculated ICC pixel oracle has been removed. ICC conversion tests
now compare against independently decoded pixels, but those exact comparisons remain failing and unresolved.
The first full codec run passed 8,837 cases and stopped on a progressive-layer reference-image mismatch.
The failing helper called provider.GetImage(), which selects the registered reference decoder; four conformance
paths now explicitly select HeifDecoder.Instance so that decoder assertions exercise the managed codec.
The encoder cases now include Png.Transparency and use existing 10/12-bit RGBA TIFF fixtures for the high-depth
test, preserving exact Rgba64 comparison. All six known-image encoder cases passed in codec-clean-output-r2;
that broad run stopped on an allocation-log hash-uniqueness assertion after 8,857 passes. Further runs are
restricted to the decoder and encoder classes. No golden image or comparison tolerance has changed.
The Windows AVIF colour/alpha disagreement remains unresolved despite the ImageMagick encoder checks passing.
ICC reference conversion now selects the same CompactSrgbV4 target profile as production. Exact decoded-pixel
comparison still fails; ImageMagick's integer RGB intermediate is not a reason to quantize our floating-point
RGB before ICC. The attempted pre-ICC rounding was reverted and its absence verified in both scalar and SIMD paths.
PNG debug output now explicitly uses PngEncoder to retain ICC metadata, and single-frame cases use DebugSave
rather than DebugSaveMultiFrame. Old output directories must be cleared before final class reruns.
Returned still and sequence CICP metadata now describes full-range RGB, retaining source primaries and transfer
when ICC is not converted. The preserve-profile PNG export passed in heif-icc-preserve-metadata-r1.
After correcting DebugSave overload selection and clearing stale output directories, heif-decoder-convention-r5
passed ten cases and stopped on the exact Ducky ICC pixel comparison. Single images now produce .png files;
the sequence alone produces a frame directory. The decoder class has not completed.
The ICC output path is being corrected to retain floating-point RGBA through final TPixel conversion, using
the shared pixel operations and existing row scratch ownership. The earlier byte/Rgba64 intermediate could
discard precision for floating-point destination formats. This change is not yet runtime-verified and does
not establish the cause or resolution of the independent ICC comparison mismatch.
Perform this cleanup at the end of implementation and verification, before final delivery. It is part of the
task, not optional follow-up work. Update this checklist and the active milestone as changes and verification land.
- [ ] Review all task-added files against the final production implementation and acceptance requirements.
- [ ] Delete temporary native reference source, integration code, libraries, executables, build output, and
generated comparison artifacts from the repository. Preserve needed local reference tooling outside the repository.
`tests/ImageSharp.Benchmarks/Codecs/Heif/Native/aom_benchmark.c` is already tracked from `a7f0fca6b`;
include its deletion and other tracked temporary reference material in the final cleanup commit.
- [ ] Delete stray task READMEs, redundant reports, temporary notes, and documentation for discarded approaches.
Retain only documentation needed for the delivered codec and its supported verification workflow.
- [ ] Delete obsolete or unnecessary tests, including tests for rejected implementations, unrequested features,
and implementation details that do not establish a required contract. Preserve independent acceptance coverage.
Do not delete or weaken a failing test merely to conceal an unresolved production or verification defect.
- [ ] Delete obsolete component benchmarks, including `Av1SequenceEncoderBenchmarks`, and their stale references.
Retained performance verification must measure full encode/decode with equivalent end-to-end boundaries.
- [ ] Inspect the final diff against upstream/main for unrelated codec changes, accidental files, and temporary
tooling. Inspect the staged changes before committing the reviewed deletions.
- [ ] Commit the deletions normally at the end. The user explicitly chose this approach: no history rewriting
and no force-push to remove earlier native-tooling commits.
## Earlier checkpoint evidence
The probability-storage checkpoint is `a658a2cb7`; adaptive syntax and retained palette tokens are `d8f6a1de3`. The probability-storage checkpoint is `a658a2cb7`; adaptive syntax and retained palette tokens are `d8f6a1de3`.
The final supporting entropy, mode-grid, frame-buffer, and intra-copy run passed 2,085 tests with zero failures. The final supporting entropy, mode-grid, frame-buffer, and intra-copy run passed 2,085 tests with zero failures.
The encoder deblocking comparison covered 102,390 samples in 12 streams at 8/10/12 bits and 400/420/422/444: The encoder deblocking comparison covered 102,390 samples in 12 streams at 8/10/12 bits and 400/420/422/444:
@ -45,9 +322,78 @@ Acceptance criteria are separate for encoding and decoding:
- Historical decoder reports of zero samples exceeding one are insufficient by themselves. A recorded maximum - Historical decoder reports of zero samples exceeding one are insufficient by themselves. A recorded maximum
error of zero establishes sample equality only for the stated comparison scope, not complete decoder correctness. error of zero establishes sample equality only for the stated comparison scope, not complete decoder correctness.
## Active production milestone: encoder motion search ## JPEG scope correction: 2026-09-07
The persistent goal remains active. Complete the integrated encoder motion-search path, including configuration, - Removed legacy JPEG item decoding and encoding, compression selection, container brands, and dedicated HEIF JPEG tests.
- Removed the single-value compression-method enum and its encoder, metadata, and item-decoder properties. Encoding selects AV1 directly; still output always writes AVIF brands.
- Restored all JPEG production files to upstream/main, including earlier branch changes to metadata writing and spectral pixel packing.
- Restored the PNG cICP writer to upstream/main. No codec-specific production changes remain outside HEIF/AV1 against upstream/main.
- Removed the added spectral pixel converter and HEIF JPEG adapter. The HEIF implementation has no JPEG codec dependency.
- General container tests retain their assertions and now generate an opaque AV1 item instead of a JPEG item.
- The decoder-region callers now compile: Roslynk reports zero compiler errors across the loaded solution.
- Release .NET 11 test assembly preparation passed. Runtime verification is in progress and the refactor is not yet a verified checkpoint.
- The Compact ICC regression is corrected: HEIF invokes ICC conversion only for Convert mode. The subsequent decoder run passed 71 cases.
- The earlier ICC implementation processed packed destination pixels using two scratch rows. It was rejected
and deleted. Component-domain ICC conversion is now implemented before pixel packing, as recorded below;
its exact-output verification remains unresolved. Earlier ICC test results do not verify the replacement.
- Deblocking transform sizes now reside in block metadata: one selected size and sixteen inline variable-transform entries.
The separate frame-sized luma/chroma maps and their ownership class are removed. Runtime verification after this change is pending.
- Historical JPEG-backed verification below does not establish acceptance for the remaining AV1-only implementation.
## Active production milestone: complete decoder-region refactor
Complete the still-image, grid, sequence, alpha, crop, rotation, and mirroring paths using exact destination regions.
Keep JPEG and other codec implementations unchanged against upstream/main. Retain the existing shared L16 SIMD implementation.
The generic RGB packer must accept exact-length regions, consistent with its existing scalar and SIMD implementations.
Complete existing caller migration and focused runtime verification before committing this checkpoint.
No new test infrastructure, extra decoder overloads, or component-test results substitute for native decoder parity.
### Transform and ICC integration: 2026-09-08, verification in progress
- The float/ICC output path now packs directly into the final region, removing its temporary TPixel row
and subsequent WriteRow copy. Destination origins and integer row/column increments are resolved once.
Contiguous output retains bulk pixel packing; reversed rows and columns currently use scalar final packing.
This is not completion of the optimized traversal: tiled/SIMD placement and end-to-end performance remain
unverified, and the outstanding ICC oracle mismatch below still prevents a verified region/ICC checkpoint.
Roslynk reported zero errors after this edit. Release/net11 preparation succeeded with 1,008 warnings and
zero errors; serialized VSTest passed 38 sequence cases (direct-region-sequences-r1.trx) and 70 public
encoder/grid cases (direct-region-grid-r1.trx). These runs do not verify the outstanding ICC mismatch.
- Rotation/mirroring matrices are prepared outside pixel loops. Row traversal uses integer destination increments;
crop and grid callers supply exact source and destination rectangles.
- ICC conversion now operates on reconstructed float components before final pixel packing, using the existing
ColorProfileConverter span APIs and configured allocator. Still images, grid tiles, and sequence frames carry
the selected profile through the same conversion boundary. Compact and Preserve retain source colors.
- Native auxiliary alpha rows are normalized/resampled before RGB unassociation and ICC conversion. Alpha is
packed only with the final pixels; the ICC path does not read back or process a packed image. Unassociation uses
planar Vector512/256/128 arithmetic without scalar alpha extraction or broadcasts.
- Scalar unassociation clips zero-alpha RGB just as the vector paths do, avoiding a row-width-dependent result
when reconstructed RGB is outside the nominal device range.
- After that final alpha edit, Release/net11 built with zero warnings/errors and `alpha-final-public-r1.trx`
passed all 70 public encoder/grid cases. Subsequent matrix-parameter and formatting corrections have Roslynk
verification only: all three Matrix3x2 parameters and six calls now pass by value, with zero compiler errors
and no production warnings. No runtime run is claimed for those later edits.
- The cropped alpha resizer's odd-column tail now selects its kernel using the destination X offset.
- Release/net11 preparation succeeded. The first runtime launch selected no tests because its filter contained
literal quotes. The corrected serialized VSTest run stopped after six passes and one ICC lookup failure.
Reconstructed RGB exceeded the device lookup interval; clipping was added during ICC interleaving without
quantizing through packed pixels. The corrected Release/net11 build succeeded with zero warnings/errors.
- `icc-transform-regions-r6.trx` passed ten cases and stopped on the existing alpha/ICC exact comparison.
Its expected image applies ICC to an already packed eight-bit preserved decode; the new component path
applies ICC before that quantization. Alpha-equality assertions passed, but RGB comparison failed. The test
and expected values remain unchanged; the numerical contract must be resolved before this checkpoint closes.
- JPEG's source path retains float normalization and profile conversion (`JpegColorConverterBase.Icc.cs:72-80,134`,
`JpegColorConverter.Packing.cs:48-50,80-82`, `ColorProfiles/YCbCr.cs:153-160`). It does not justify adding an
eight-bit rounding stage solely to reproduce the old packed-image test oracle.
- Independent focused runs passed 70 public encoder/grid cases (`transform-grid-regions-r1.trx`), 38 sequence
cases (`transform-sequence-regions-r1.trx`), and 12 retained native-reference presentation cases at 8/10/12 bits
and monochrome/420/422/444 (`native-presented-regions-r1.trx`). The last group uses exact image comparison
across configured intrinsic paths. These passing groups do not clear the outstanding ICC failure.
- No benchmark or codec-wide parity claim follows from these edits. Earlier ICC results used the rejected
post-packing implementation and do not verify this implementation.
## Remaining encoder milestone: motion search
The overall codec goal remains unresolved. After the decoder refactor, complete the integrated encoder motion-search path, including configuration,
allocation geometry, rate costs, candidate and winner state, full-pixel search, fractional refinement, and production allocation geometry, rate costs, candidate and winner state, full-pixel search, fractional refinement, and production
verification before advancing. The following dependency result does not close that milestone. verification before advancing. The following dependency result does not close that milestone.
@ -2759,7 +3105,7 @@ Writer primitives are not an encoder. The public encoder remains incomplete unti
- [x] Use the existing PNG, TIFF, and JPEG encoders as the ImageSharp architecture reference: generic `Image<TPixel>` input, encoder options taking precedence over converted format metadata and codec defaults, allocator-owned temporary storage, and deterministic disposal. - [x] Use the existing PNG, TIFF, and JPEG encoders as the ImageSharp architecture reference: generic `Image<TPixel>` input, encoder options taking precedence over converted format metadata and codec defaults, allocator-owned temporary storage, and deterministic disposal.
- [x] Treat source pixel type, source alpha representation, and decoded source bit depth as conversion inputs, never as output-eligibility checks. Do not pre-scan pixels before encoding. - [x] Treat source pixel type, source alpha representation, and decoded source bit depth as conversion inputs, never as output-eligibility checks. Do not pre-scan pixels before encoding.
- [x] Resolve output configuration once from explicit encoder options, converted `HeifMetadata`, and AV1 defaults in that order. Sanitize only combinations that cannot describe a legal requested output, and never write resolved values back to source metadata. - [x] Resolve output configuration once from explicit encoder options, converted `HeifMetadata`, and AV1 defaults in that order. Sanitize only combinations that cannot describe a legal requested output, and never write resolved values back to source metadata.
- [~] Finalize observable options for quality, effort, lossless mode, bit depth, chroma subsampling, alpha quality, metadata, and bounded sequences. Sequence repeat-count options override converted HEIF metadata like the existing animated encoders. Legacy JPEG treats the AV1-specific lossless and bit-depth options as inapplicable and continues with its native eight-bit encoding contract. AV1 sequences now follow the existing animated-image contract: the primary item reuses the first sync sample when the root is animated, while an excluded root is encoded once as the independent primary image and the sequence begins at frame index one. Final verification remains open. - [~] Finalize observable options for quality, effort, lossless mode, bit depth, chroma subsampling, alpha quality, metadata, and bounded sequences. Sequence repeat-count options override converted HEIF metadata like the existing animated encoders. AV1 sequences now follow the existing animated-image contract: the primary item reuses the first sync sample when the root is animated, while an excluded root is encoded once as the independent primary image and the sequence begins at frame index one. Final verification remains open.
- [x] Preserve high-bit-depth source precision through 16-bit RGB and native 10/12-bit component planes. - [x] Preserve high-bit-depth source precision through 16-bit RGB and native 10/12-bit component planes.
- [~] HEIF is registered through the default configuration module. Keep public AV1 capability claims limited to the paths covered by the encoder verification matrix until the remaining encoder work is complete. - [~] HEIF is registered through the default configuration module. Keep public AV1 capability claims limited to the paths covered by the encoder verification matrix until the remaining encoder work is complete.
@ -2850,7 +3196,7 @@ Encoder verification contract:
- [~] AV1 image properties now write `ispe`, `pixi`, `av1C`, `colr`, and `auxC` in current AVIF item order. Only `av1C` is essential; color and alpha items retain independent property sets and the registered alpha auxiliary type. The property container reacquires its span after nested expansion before patching `ipco`, removing the prior stale-buffer write, and selects compact or 15-bit `ipma` indices from the property count rather than the unrelated item count. A forced-growth color-plus-alpha case validates every property payload and association byte; a separate 43-item, 129-property case proves indices 127 through 129 and the extended essential bit. Both pass direct foreground net11 Release VSTest. Complete AVIF assembly remains open. - [~] AV1 image properties now write `ispe`, `pixi`, `av1C`, `colr`, and `auxC` in current AVIF item order. Only `av1C` is essential; color and alpha items retain independent property sets and the registered alpha auxiliary type. The property container reacquires its span after nested expansion before patching `ipco`, removing the prior stale-buffer write, and selects compact or 15-bit `ipma` indices from the property count rather than the unrelated item count. A forced-growth color-plus-alpha case validates every property payload and association byte; a separate 43-item, 129-property case proves indices 127 through 129 and the extended essential bit. Both pass direct foreground net11 Release VSTest. Complete AVIF assembly remains open.
- [~] Explicit public AV1 encoding now writes a still-image AVIF with `avif` major brand, compatible `avif`, `mif1`, and `miaf` brands, one primary color item, an optional alpha auxiliary item, `auxl` from alpha to color, independent item properties, absolute version-one `iloc` extents, and a shared `mdat`. Quality uses current libaom's quantizer-to-qindex mapping with public quality 100 deliberately clamped from lossless qindex 0 to qindex 4. Effort controls the implemented search stages, and the resolved value is required explicitly by every internal frame, tile, and mode-decision operation rather than repeated as optional defaults. Encoder options take precedence over source metadata for 8-, 10-, and 12-bit monochrome, 4:2:0, 4:2:2, and 4:4:4 output. Alpha derives from the source pixel type without scanning pixels, and incompatible identity-matrix metadata is normalized without mutating the source image. - [~] Explicit public AV1 encoding now writes a still-image AVIF with `avif` major brand, compatible `avif`, `mif1`, and `miaf` brands, one primary color item, an optional alpha auxiliary item, `auxl` from alpha to color, independent item properties, absolute version-one `iloc` extents, and a shared `mdat`. Quality uses current libaom's quantizer-to-qindex mapping with public quality 100 deliberately clamped from lossless qindex 0 to qindex 4. Effort controls the implemented search stages, and the resolved value is required explicitly by every internal frame, tile, and mode-decision operation rather than repeated as optional defaults. Encoder options take precedence over source metadata for 8-, 10-, and 12-bit monochrome, 4:2:0, 4:2:2, and 4:4:4 output. Alpha derives from the source pixel type without scanning pixels, and incompatible identity-matrix metadata is normalized without mutating the source image.
- [~] The production path writes color and alpha payloads sequentially through allocator-backed chunked storage, supports non-seekable and prefixed destinations, and does not materialize a complete file or payload copy. Uniform encoder-side `pixi` depth is written directly without allocating per-item channel-depth arrays; decoder-side non-uniform channel depths remain supported. The Release test project builds with zero errors, all 39 encoder cases pass, the complete non-HEVC HEIF namespace passes 9,277 of 9,277, and current official libaom accepts all 47 generated payloads. - [~] The production path writes color and alpha payloads sequentially through allocator-backed chunked storage, supports non-seekable and prefixed destinations, and does not materialize a complete file or payload copy. Uniform encoder-side `pixi` depth is written directly without allocating per-item channel-depth arrays; decoder-side non-uniform channel depths remain supported. The Release test project builds with zero errors, all 39 encoder cases pass, the complete non-HEVC HEIF namespace passes 9,277 of 9,277, and current official libaom accepts all 47 generated payloads.
- [x] Still-image AVIF metadata preservation now writes an unrestricted ICC `colr/prof` property before the independent `colr/nclx` property, Exif and XMP as separate `mdat` items, and one `cdsc` relationship from each metadata item to the primary color item. Exif stores the exact big-endian TIFF-header offset required by the HEIF item syntax; XMP uses the `mime` item type and `application/rdf+xml` content type. Existing ICC and XMP storage is read synchronously and copied once into final encoder storage rather than cloned into an intermediate array. `SkipMetadata` suppresses all three profile types while retaining the CICP values required to describe the encoded planes. The same option now reaches legacy JPEG payloads, whose encoder no longer writes application profiles or comments when metadata is disabled. - [x] Still-image AVIF metadata preservation now writes an unrestricted ICC `colr/prof` property before the independent `colr/nclx` property, Exif and XMP as separate `mdat` items, and one `cdsc` relationship from each metadata item to the primary color item. Exif stores the exact big-endian TIFF-header offset required by the HEIF item syntax; XMP uses the `mime` item type and `application/rdf+xml` content type. Existing ICC and XMP storage is read synchronously and copied once into final encoder storage rather than cloned into an intermediate array. `SkipMetadata` suppresses all three profile types while retaining the CICP values required to describe the encoded planes.
- [x] Exact container tests verify every emitted item declaration, name, MIME content type, `cdsc` relationship, Exif offset and payload, XMP payload, ICC/CICP property order, compact association byte, propertyless metadata exclusion, decoded profile value, and both `SkipMetadata` branches. The final HEIF encoder set passes 44 of 44 and the complete JPEG encoder set passes 257 of 257 through direct foreground net11 Release VSTest. The complete non-HEVC HEIF namespace passes 9,282 of 9,282 with no failure, crash, or detached test host, and current official libaom accepts all 47 current generated AV1 payloads. - [x] Exact container tests verify every emitted item declaration, name, MIME content type, `cdsc` relationship, Exif offset and payload, XMP payload, ICC/CICP property order, compact association byte, propertyless metadata exclusion, decoded profile value, and both `SkipMetadata` branches. The final HEIF encoder set passes 44 of 44 and the complete JPEG encoder set passes 257 of 257 through direct foreground net11 Release VSTest. The complete non-HEVC HEIF namespace passes 9,282 of 9,282 with no failure, crash, or detached test host, and current official libaom accepts all 47 current generated AV1 payloads.
- [x] A code-wide production HEIF/AV1 stack-storage audit, excluding HEVC, removed every block-sized, variable-length, or repeatedly nested scratch buffer. Spatial luma and chroma, filter-intra, chroma-from-luma, luma and chroma palette selection, and K-means iteration now use typed views over 642 signed-integer elements, about 2.51 KiB, at the start of the shared inter-prediction region. Those searches are sequential for one block, so the block-workspace owner does not grow and no rent, copy, or additional lifetime is introduced. CDEF directions, variances, and its 64-entry block list now append 1 KiB to the existing bounded operation owner instead of occupying hidden inline or explicit stack arrays. No remaining `stackalloc` depends on block dimensions, sample count, or runtime length; the largest remaining individual span is 128 bytes, and the remaining sites are fixed syntax, SIMD-lane, filter-tap, plane-metadata, or small candidate storage. The exact-owner test now proves the mode, palette, and reference-prediction views share one allocation. Roslynk reports zero compiler errors and no diagnostics in the changed files, the Release test-project build completes with the established 1,992 warnings and zero errors, 81 of 81 focused cases pass, and the complete non-HEVC HEIF/AV1 namespace passes 9,282 of 9,282 through one foreground net11 VSTest run. - [x] A code-wide production HEIF/AV1 stack-storage audit, excluding HEVC, removed every block-sized, variable-length, or repeatedly nested scratch buffer. Spatial luma and chroma, filter-intra, chroma-from-luma, luma and chroma palette selection, and K-means iteration now use typed views over 642 signed-integer elements, about 2.51 KiB, at the start of the shared inter-prediction region. Those searches are sequential for one block, so the block-workspace owner does not grow and no rent, copy, or additional lifetime is introduced. CDEF directions, variances, and its 64-entry block list now append 1 KiB to the existing bounded operation owner instead of occupying hidden inline or explicit stack arrays. No remaining `stackalloc` depends on block dimensions, sample count, or runtime length; the largest remaining individual span is 128 bytes, and the remaining sites are fixed syntax, SIMD-lane, filter-tap, plane-metadata, or small candidate storage. The exact-owner test now proves the mode, palette, and reference-prediction views share one allocation. Roslynk reports zero compiler errors and no diagnostics in the changed files, the Release test-project build completes with the established 1,992 warnings and zero errors, 81 of 81 focused cases pass, and the complete non-HEVC HEIF/AV1 namespace passes 9,282 of 9,282 through one foreground net11 VSTest run.
- [~] Bounded public image-sequence output now emits an `avis` movie with version-one movie, track, and media headers; AV1 visual sample entries; exact run-length-compressed timing; per-sample sizes; 64-bit chunk offsets; and an explicit sync-sample table. The file type includes the required `miaf` compatibility brand, and every sequence now has the MIAF primary image item emitted by current libavif: normal sequences share the first sync-sample extent without another encode or copy, while separate-root sequences retain the still root as the primary image and begin timed samples at frame index one. The decoder allocates one final `Image<TPixel>`: the root is either the first timed sample or the separately decoded primary item, and each visible timed sample is decoded directly into a frame owned by that image. Exact quarter-turn presentation uses one frame-sized reusable pre-rotation buffer rather than a second image or a separately built frame collection. Color and optional auxiliary alpha use independently configured AV1 tracks linked by `auxl`. Lossless samples remain independently decodable key pictures and repeat the sequence header required for random access. Lossy continuation samples use LAST_FRAME inter prediction through the existing SIMD translational predictor; one track-scoped encoder session reuses its source allocation, packed-to-planar row storage and color converter, frame-sized coefficient storage, fixed-geometry picture and frame-header syntax state, tile/superblock/entropy cursors, block arithmetic workspace, complete probability graph, bounded tile-output owner, and OBU-header owner, and swaps two complete reconstruction buffers so the preceding decoded frame becomes the next reference without a plane copy. Sequence samples signal `still_picture=0` and use the complete non-reduced sequence and frame-header prefixes required for a multi-frame coded sequence. Frame payloads are written once into contiguous allocator-backed chunks per track. One compact managed table retains only offset, length, and duration for both tracks, and the bounded `moov` owner is patched once after its final size is known, so prefixed and non-seekable destinations require neither seeking nor a file-sized copy. The media timescale uses the exact representable least common multiple of animated frame-delay denominators and a documented microsecond fallback; zero delays become the smallest legal positive duration. Public lossless color-and-alpha round trips preserve all frames, distinct 24, 25, and 30 fps delays, finite or infinite repetition, ICC, Exif, and XMP metadata, while a separate case proves prefixed non-seekable output. Per-tile CDEF preset, preceding-quantizer state, and payload bounds now occupy one aligned region in the reusable allocator-owned picture buffer rather than separate managed arrays for every frame. The last verified net11 Release checkpoint completed with the established 1,005 warnings and zero errors, all 48 HEIF encoder cases passed, and the complete non-HEVC HEIF/AV1 namespace passed 9,317 of 9,317 through foreground VSTest. Current official libaom `main` at `d565eec60f084421fa34fc0534b760c6452b6a6c` accepted all 66 raw AV1 payloads regenerated by that suite. Verification of the current primary-item, separate-root, non-reduced sequence-header, retained-reference continuation, grid implementation, block-local motion search, conversion-row, probability, picture, frame-header, tile-cursor, tile-output, and OBU-header reuse, and multi-tile output is pending. Additional reference roles and compound prediction remain open. - [~] Bounded public image-sequence output now emits an `avis` movie with version-one movie, track, and media headers; AV1 visual sample entries; exact run-length-compressed timing; per-sample sizes; 64-bit chunk offsets; and an explicit sync-sample table. The file type includes the required `miaf` compatibility brand, and every sequence now has the MIAF primary image item emitted by current libavif: normal sequences share the first sync-sample extent without another encode or copy, while separate-root sequences retain the still root as the primary image and begin timed samples at frame index one. The decoder allocates one final `Image<TPixel>`: the root is either the first timed sample or the separately decoded primary item, and each visible timed sample is decoded directly into a frame owned by that image. Exact quarter-turn presentation uses one frame-sized reusable pre-rotation buffer rather than a second image or a separately built frame collection. Color and optional auxiliary alpha use independently configured AV1 tracks linked by `auxl`. Lossless samples remain independently decodable key pictures and repeat the sequence header required for random access. Lossy continuation samples use LAST_FRAME inter prediction through the existing SIMD translational predictor; one track-scoped encoder session reuses its source allocation, packed-to-planar row storage and color converter, frame-sized coefficient storage, fixed-geometry picture and frame-header syntax state, tile/superblock/entropy cursors, block arithmetic workspace, complete probability graph, bounded tile-output owner, and OBU-header owner, and swaps two complete reconstruction buffers so the preceding decoded frame becomes the next reference without a plane copy. Sequence samples signal `still_picture=0` and use the complete non-reduced sequence and frame-header prefixes required for a multi-frame coded sequence. Frame payloads are written once into contiguous allocator-backed chunks per track. One compact managed table retains only offset, length, and duration for both tracks, and the bounded `moov` owner is patched once after its final size is known, so prefixed and non-seekable destinations require neither seeking nor a file-sized copy. The media timescale uses the exact representable least common multiple of animated frame-delay denominators and a documented microsecond fallback; zero delays become the smallest legal positive duration. Public lossless color-and-alpha round trips preserve all frames, distinct 24, 25, and 30 fps delays, finite or infinite repetition, ICC, Exif, and XMP metadata, while a separate case proves prefixed non-seekable output. Per-tile CDEF preset, preceding-quantizer state, and payload bounds now occupy one aligned region in the reusable allocator-owned picture buffer rather than separate managed arrays for every frame. The last verified net11 Release checkpoint completed with the established 1,005 warnings and zero errors, all 48 HEIF encoder cases passed, and the complete non-HEVC HEIF/AV1 namespace passed 9,317 of 9,317 through foreground VSTest. Current official libaom `main` at `d565eec60f084421fa34fc0534b760c6452b6a6c` accepted all 66 raw AV1 payloads regenerated by that suite. Verification of the current primary-item, separate-root, non-reduced sequence-header, retained-reference continuation, grid implementation, block-local motion search, conversion-row, probability, picture, frame-header, tile-cursor, tile-output, and OBU-header reuse, and multi-tile output is pending. Additional reference roles and compound prediction remain open.

6
src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs

@ -415,6 +415,10 @@ internal sealed class Av1CodecConfiguration
bool highBitDepth = colorConfig.BitDepth is Av1BitDepth.TenBit or Av1BitDepth.TwelveBit; bool highBitDepth = colorConfig.BitDepth is Av1BitDepth.TenBit or Av1BitDepth.TwelveBit;
bool twelveBit = colorConfig.BitDepth == Av1BitDepth.TwelveBit; bool twelveBit = colorConfig.BitDepth == Av1BitDepth.TwelveBit;
// Chroma sample position is signaled only for 4:2:0. Other layouts have no corresponding
// sequence-header field, so their container value cannot be compared with the parser default.
bool hasChromaSamplePosition = !colorConfig.IsMonochrome && colorConfig.SubSamplingX && colorConfig.SubSamplingY;
if (this.SequenceProfile != (byte)sequenceHeader.SequenceProfile if (this.SequenceProfile != (byte)sequenceHeader.SequenceProfile
|| this.SequenceLevelIndex != operatingPoint.SequenceLevelIndex || this.SequenceLevelIndex != operatingPoint.SequenceLevelIndex
|| this.SequenceTier != (operatingPoint.SequenceTier != 0) || this.SequenceTier != (operatingPoint.SequenceTier != 0)
@ -423,7 +427,7 @@ internal sealed class Av1CodecConfiguration
|| this.IsMonochrome != colorConfig.IsMonochrome || this.IsMonochrome != colorConfig.IsMonochrome
|| this.ChromaSubsamplingX != colorConfig.SubSamplingX || this.ChromaSubsamplingX != colorConfig.SubSamplingX
|| this.ChromaSubsamplingY != colorConfig.SubSamplingY || this.ChromaSubsamplingY != colorConfig.SubSamplingY
|| this.ChromaSamplePosition != (byte)colorConfig.ChromaSamplePosition) || (hasChromaSamplePosition && this.ChromaSamplePosition != (byte)colorConfig.ChromaSamplePosition))
{ {
throw new InvalidImageContentException("The AV1 item configuration does not match its sequence header."); throw new InvalidImageContentException("The AV1 item configuration does not match its sequence header.");
} }

256
src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs

@ -15,6 +15,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.PixelFormats.Utils; using SixLabors.ImageSharp.PixelFormats.Utils;
@ -167,125 +168,14 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
public Av1InterPredictionFeatures DecodedInterPredictionFeatures { get; private set; } public Av1InterPredictionFeatures DecodedInterPredictionFeatures { get; private set; }
/// <summary> /// <summary>
/// Gets the native planes of the current retained shown frame, or <see langword="null"/> before one completes. /// Gets or sets the chroma reconstruction mode for presented sequence frames.
/// </summary>
public Av1FrameBuffer<byte>? FrameBuffer => this.referenceFrames.OutputFrame?.FrameBuffer;
/// <summary>
/// Decodes a bounded AV1 image payload and presents its final shown frame.
/// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
/// <param name="buffer">The complete AV1 elementary-stream payload.</param>
/// <param name="containerColorProfile">
/// The container color description that supplies unspecified sequence-header color information.
/// </param>
/// <param name="codecConfiguration">
/// The item-associated AV1 codec configuration validated against the coded sequence header.
/// </param>
/// <param name="layeredImageIndex">The optional byte boundaries of a layered AV1 image item.</param>
/// <param name="presentationSize">The requested item presentation size, or an empty size for the coded dimensions.</param>
/// <returns>The decoded image.</returns>
public Image<TPixel> Decode<TPixel>(
Span<byte> buffer,
CicpProfile? containerColorProfile = null,
Av1CodecConfiguration? codecConfiguration = null,
Av1LayeredImageIndex? layeredImageIndex = null,
Size presentationSize = default)
where TPixel : unmanaged, IPixel<TPixel>
{
ImageFrame<TPixel> frame = this.DecodeFrame<TPixel>(
buffer,
containerColorProfile,
codecConfiguration,
out CicpProfile effectiveColorProfile,
layeredImageIndex,
presentationSize);
ImageMetadata metadata = new()
{
CicpProfile = effectiveColorProfile
};
HeifContentLightLevel? contentLightLevel = this.obuReader.ContentLightLevel;
HeifMasteringDisplayColorVolume? masteringDisplayColorVolume = this.obuReader.MasteringDisplayColorVolume;
if (contentLightLevel is not null || masteringDisplayColorVolume is not null)
{
HeifMetadata heifMetadata = metadata.GetHeifMetadata();
heifMetadata.ContentLightLevel = contentLightLevel;
heifMetadata.MasteringDisplayColorVolume = masteringDisplayColorVolume;
}
try
{
return new Image<TPixel>(this.configuration, metadata, [frame]);
}
catch
{
// Ownership transfers only after the image constructor accepts the decoded frame.
frame.Dispose();
throw;
}
}
/// <summary>
/// Decodes an AV1 elementary-stream payload into one independently owned ImageSharp frame.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam> public HeifChromaUpsampling ChromaUpsampling { get; set; }
/// <param name="buffer">The complete AV1 elementary-stream payload.</param>
/// <param name="containerColorProfile">
/// The container color description that supplies unspecified sequence-header color information.
/// </param>
/// <param name="codecConfiguration">
/// The AV1 codec configuration validated against the coded sequence header.
/// </param>
/// <param name="effectiveColorProfile">Receives the effective CICP description used for conversion.</param>
/// <param name="layeredImageIndex">The optional byte boundaries of a layered AV1 image item.</param>
/// <param name="presentationSize">The requested item presentation size, or an empty size for the coded dimensions.</param>
/// <returns>The decoded frame. Ownership transfers to the caller.</returns>
public ImageFrame<TPixel> DecodeFrame<TPixel>(
Span<byte> buffer,
CicpProfile? containerColorProfile,
Av1CodecConfiguration? codecConfiguration,
out CicpProfile effectiveColorProfile,
Av1LayeredImageIndex? layeredImageIndex = null,
Size presentationSize = default)
where TPixel : unmanaged, IPixel<TPixel>
{
using Av1FrameBuffer<byte> frameBuffer = this.DecodeFrameBuffer(
buffer,
containerColorProfile,
codecConfiguration,
out effectiveColorProfile,
out ObuFrameHeader frameHeader,
layeredImageIndex);
return this.ConvertToFrame<TPixel>(frameBuffer, frameHeader, effectiveColorProfile, presentationSize);
}
/// <summary> /// <summary>
/// Decodes the next visible sample in a bounded AV1 image sequence while retaining its reference state. /// Gets the native planes of the current retained shown frame, or <see langword="null"/> before one completes.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam> public Av1FrameBuffer<byte>? FrameBuffer => this.referenceFrames.OutputFrame?.FrameBuffer;
/// <param name="buffer">The complete AV1 sample payload.</param>
/// <param name="containerColorProfile">The container color description.</param>
/// <param name="codecConfiguration">The AV1 sample-entry configuration.</param>
/// <returns>The independently owned decoded frame.</returns>
public ImageFrame<TPixel> DecodeSequenceFrame<TPixel>(
Span<byte> buffer,
CicpProfile? containerColorProfile,
Av1CodecConfiguration? codecConfiguration)
where TPixel : unmanaged, IPixel<TPixel>
{
CicpProfile effectiveColorProfile = this.DecodePayload(
buffer,
containerColorProfile,
codecConfiguration,
null,
requireShownFrame: true);
Av1ReferenceFrame outputFrame = this.referenceFrames.ResolveOutput();
return this.ConvertToFrame<TPixel>(outputFrame.FrameBuffer, outputFrame.FrameHeader, effectiveColorProfile);
}
/// <summary> /// <summary>
/// Decodes the next visible sample in a bounded AV1 image sequence directly into a caller-owned frame. /// Decodes the next visible sample in a bounded AV1 image sequence directly into a caller-owned frame.
@ -297,13 +187,21 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
/// <param name="expectedCodedSize">The coded dimensions declared by the visual sample entry.</param> /// <param name="expectedCodedSize">The coded dimensions declared by the visual sample entry.</param>
/// <param name="sourceRectangle">The clean-aperture region mapped to the complete destination frame.</param> /// <param name="sourceRectangle">The clean-aperture region mapped to the complete destination frame.</param>
/// <param name="destination">The caller-owned packed-pixel frame receiving the presented sample.</param> /// <param name="destination">The caller-owned packed-pixel frame receiving the presented sample.</param>
public void DecodeSequenceFrame<TPixel>( /// <param name="transform">The rotation and mirroring applied within the destination region.</param>
/// <param name="profile">The source profile selected for conversion, or null to preserve source colors.</param>
/// <param name="alphaFrame">The decoder-owned auxiliary frame, or null for opaque pixels.</param>
/// <param name="premultiplied">Whether source RGB is associated with alpha.</param>
public CicpProfile DecodeSequenceFrame<TPixel>(
Span<byte> buffer, Span<byte> buffer,
CicpProfile? containerColorProfile, CicpProfile? containerColorProfile,
Av1CodecConfiguration? codecConfiguration, Av1CodecConfiguration? codecConfiguration,
Size expectedCodedSize, Size expectedCodedSize,
Rectangle sourceRectangle, Rectangle sourceRectangle,
ImageFrame<TPixel> destination) Buffer2DRegion<TPixel> destination,
HeifPixelTransform transform,
IccProfile? profile,
Av1FrameBuffer<byte>? alphaFrame,
bool premultiplied)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
CicpProfile effectiveColorProfile = this.DecodePayload( CicpProfile effectiveColorProfile = this.DecodePayload(
@ -324,13 +222,23 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
"The decoded image-sequence sample dimensions do not match its visual sample entry."); "The decoded image-sequence sample dimensions do not match its visual sample entry.");
} }
Av1YuvConverter.ConvertRegionToRgb( // Keep reconstructed reference state unchanged while honoring the container's presentation range.
Av1YuvConverter.ConvertToRgb(
this.configuration, this.configuration,
outputFrame.FrameBuffer, outputFrame.FrameBuffer,
sourceRectangle, sourceRectangle,
destination); destination,
codedSize,
transform,
profile,
alphaFrame,
codedSize,
sourceRectangle,
premultiplied,
this.ChromaUpsampling,
containerColorProfile?.FullRange ?? outputFrame.FrameBuffer.ColorConfig.ColorRange);
destination.Metadata.CicpProfile = effectiveColorProfile.DeepClone(); return effectiveColorProfile;
} }
/// <summary> /// <summary>
@ -351,29 +259,18 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
requireShownFrame: false); requireShownFrame: false);
/// <summary> /// <summary>
/// Decodes the next visible monochrome AV1 sequence sample and composes it into a color frame. /// Decodes the next visible monochrome AV1 sequence sample into native samples.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The destination color pixel type.</typeparam>
/// <param name="buffer">The complete AV1 sample payload.</param> /// <param name="buffer">The complete AV1 sample payload.</param>
/// <param name="containerColorProfile">The container color description.</param> /// <param name="containerColorProfile">The container color description.</param>
/// <param name="codecConfiguration">The AV1 sample-entry configuration.</param> /// <param name="codecConfiguration">The AV1 sample-entry configuration.</param>
/// <param name="expectedCodedSize">The required coded dimensions.</param> /// <param name="expectedCodedSize">The required coded dimensions.</param>
/// <param name="sourceRectangle">The clean-aperture luma region mapped to the destination.</param> /// <returns>The auxiliary frame retained by this decoder until the next sample is decoded or the decoder is disposed.</returns>
/// <param name="destination">The packed color frame receiving alpha values.</param> public Av1FrameBuffer<byte> DecodeSequenceAlpha(
/// <param name="outputSize">The complete presented size of the auxiliary image.</param>
/// <param name="destinationRectangle">The destination region receiving the alpha image.</param>
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
public void DecodeSequenceAlpha<TPixel>(
Span<byte> buffer, Span<byte> buffer,
CicpProfile? containerColorProfile, CicpProfile? containerColorProfile,
Av1CodecConfiguration? codecConfiguration, Av1CodecConfiguration? codecConfiguration,
Size expectedCodedSize, Size expectedCodedSize)
Rectangle sourceRectangle,
ImageFrame<TPixel> destination,
Size outputSize,
Rectangle destinationRectangle,
bool premultiplied)
where TPixel : unmanaged, IPixel<TPixel>
{ {
_ = this.DecodePayload( _ = this.DecodePayload(
buffer, buffer,
@ -383,59 +280,18 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
requireShownFrame: true); requireShownFrame: true);
Av1ReferenceFrame outputFrame = this.referenceFrames.ResolveOutput(); Av1ReferenceFrame outputFrame = this.referenceFrames.ResolveOutput();
this.ComposeAlpha( Av1FrameBuffer<byte> frame = outputFrame.FrameBuffer;
outputFrame.FrameBuffer, if (frame.Width != expectedCodedSize.Width || frame.Height != expectedCodedSize.Height)
expectedCodedSize,
sourceRectangle,
destination,
outputSize,
destinationRectangle,
premultiplied);
}
/// <summary>
/// Converts native AV1 planes into one independently owned packed-pixel frame.
/// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
/// <param name="frameBuffer">The decoded native planes.</param>
/// <param name="frameHeader">The completed header describing the decoded native planes.</param>
/// <param name="effectiveColorProfile">The effective CICP description.</param>
/// <param name="presentationSize">The requested item presentation size, or an empty size for the coded dimensions.</param>
/// <returns>The independently owned packed-pixel frame.</returns>
private ImageFrame<TPixel> ConvertToFrame<TPixel>(
Av1FrameBuffer<byte> frameBuffer,
ObuFrameHeader frameHeader,
CicpProfile effectiveColorProfile,
Size presentationSize = default)
where TPixel : unmanaged, IPixel<TPixel>
{
ImageFrame<TPixel>? resultFrame = null;
try
{ {
Size codedSize = new( throw new InvalidImageContentException("The decoded alpha sample dimensions do not match its visual sample entry.");
frameHeader.FrameSize.SuperResolutionUpscaledWidth,
frameHeader.FrameSize.FrameHeight);
// A selected lower spatial layer can only be scaled upward to the image item's ispe extent here.
// Other item-size corrections keep using the shared packed-pixel presentation path after decoding.
Size outputSize = presentationSize.Width >= codedSize.Width && presentationSize.Height >= codedSize.Height
? presentationSize
: codedSize;
resultFrame = new ImageFrame<TPixel>(
this.configuration,
outputSize.Width,
outputSize.Height);
Av1YuvConverter.ConvertToRgb(this.configuration, frameBuffer, resultFrame);
resultFrame.Metadata.CicpProfile = effectiveColorProfile.DeepClone();
return resultFrame;
} }
catch
if (frame.ColorFormat != Av1ColorFormat.Yuv400)
{ {
resultFrame?.Dispose(); throw new InvalidImageContentException("An AV1 alpha sample must be monochrome.");
throw;
} }
return frame;
} }
/// <summary> /// <summary>
@ -452,16 +308,18 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
/// <param name="outputSize">The complete presented size of the auxiliary image or grid tile.</param> /// <param name="outputSize">The complete presented size of the auxiliary image or grid tile.</param>
/// <param name="destinationRectangle">The destination region receiving the top-left portion of the presented alpha image.</param> /// <param name="destinationRectangle">The destination region receiving the top-left portion of the presented alpha image.</param>
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param> /// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
/// <param name="transform">The rotation and mirroring applied within the destination region.</param>
/// <param name="layeredImageIndex">The optional byte boundaries of a layered AV1 image item.</param> /// <param name="layeredImageIndex">The optional byte boundaries of a layered AV1 image item.</param>
public void DecodeAlpha<TPixel>( public void DecodeAlpha<TPixel>(
Span<byte> buffer, Span<byte> buffer,
CicpProfile? containerColorProfile, CicpProfile? containerColorProfile,
Av1CodecConfiguration? codecConfiguration, Av1CodecConfiguration? codecConfiguration,
Size expectedCodedSize, Size expectedCodedSize,
ImageFrame<TPixel> destination, Buffer2DRegion<TPixel> destination,
Size outputSize, Size outputSize,
Rectangle destinationRectangle, Rectangle destinationRectangle,
bool premultiplied, bool premultiplied,
HeifPixelTransform transform,
Av1LayeredImageIndex? layeredImageIndex = null) Av1LayeredImageIndex? layeredImageIndex = null)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
@ -478,7 +336,8 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
destination, destination,
outputSize, outputSize,
destinationRectangle, destinationRectangle,
premultiplied); premultiplied,
transform);
} }
/// <summary> /// <summary>
@ -491,13 +350,15 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
/// <param name="outputSize">The complete presented size of the auxiliary image.</param> /// <param name="outputSize">The complete presented size of the auxiliary image.</param>
/// <param name="destinationRectangle">The destination region receiving the alpha image.</param> /// <param name="destinationRectangle">The destination region receiving the alpha image.</param>
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param> /// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
/// <param name="transform">The rotation and mirroring applied within the destination region.</param>
private void ComposeAlpha<TPixel>( private void ComposeAlpha<TPixel>(
Av1FrameBuffer<byte> frameBuffer, Av1FrameBuffer<byte> frameBuffer,
Size expectedCodedSize, Size expectedCodedSize,
ImageFrame<TPixel> destination, Buffer2DRegion<TPixel> destination,
Size outputSize, Size outputSize,
Rectangle destinationRectangle, Rectangle destinationRectangle,
bool premultiplied) bool premultiplied,
HeifPixelTransform transform)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
=> this.ComposeAlpha( => this.ComposeAlpha(
frameBuffer, frameBuffer,
@ -506,7 +367,8 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
destination, destination,
outputSize, outputSize,
destinationRectangle, destinationRectangle,
premultiplied); premultiplied,
transform);
/// <summary> /// <summary>
/// Composes one decoded monochrome region into a packed color frame. /// Composes one decoded monochrome region into a packed color frame.
@ -519,14 +381,16 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
/// <param name="outputSize">The complete presented size of the auxiliary image.</param> /// <param name="outputSize">The complete presented size of the auxiliary image.</param>
/// <param name="destinationRectangle">The destination region receiving the alpha image.</param> /// <param name="destinationRectangle">The destination region receiving the alpha image.</param>
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param> /// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
/// <param name="transform">The rotation and mirroring applied within the destination region.</param>
private void ComposeAlpha<TPixel>( private void ComposeAlpha<TPixel>(
Av1FrameBuffer<byte> frameBuffer, Av1FrameBuffer<byte> frameBuffer,
Size expectedCodedSize, Size expectedCodedSize,
Rectangle sourceRectangle, Rectangle sourceRectangle,
ImageFrame<TPixel> destination, Buffer2DRegion<TPixel> destination,
Size outputSize, Size outputSize,
Rectangle destinationRectangle, Rectangle destinationRectangle,
bool premultiplied) bool premultiplied,
HeifPixelTransform transform)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (expectedCodedSize != default && (frameBuffer.Width != expectedCodedSize.Width || frameBuffer.Height != expectedCodedSize.Height)) if (expectedCodedSize != default && (frameBuffer.Width != expectedCodedSize.Width || frameBuffer.Height != expectedCodedSize.Height))
@ -548,7 +412,8 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
destination, destination,
outputSize, outputSize,
destinationRectangle, destinationRectangle,
premultiplied); premultiplied,
transform);
} }
/// <summary> /// <summary>
@ -758,11 +623,6 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
{ {
throw new InvalidImageContentException("The HEIF CICP matrix coefficients do not match the AV1 sequence header."); throw new InvalidImageContentException("The HEIF CICP matrix coefficients do not match the AV1 sequence header.");
} }
if (colorConfig.ColorRange != colorProfile.FullRange)
{
throw new InvalidImageContentException("The HEIF CICP color range does not match the AV1 sequence header.");
}
} }
// The same sequence header governs subsequent layered frames until another header OBU replaces it. // The same sequence header governs subsequent layered frames until another header OBU replaces it.
@ -1046,7 +906,6 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
// A non-shown frame or failed reconstruction never escapes this callback. The tile reader releases the // A non-shown frame or failed reconstruction never escapes this callback. The tile reader releases the
// reconstruction lease; a retained frame keeps only its compact reference state after neighbor contexts // reconstruction lease; a retained frame keeps only its compact reference state after neighbor contexts
// and the remaining frame-sized syntax are returned. // and the remaining frame-sized syntax are returned.
frameDecoder?.Dispose();
presentationBuffer?.Dispose(); presentationBuffer?.Dispose();
frameBuffer?.Dispose(); frameBuffer?.Dispose();
tileReader?.Dispose(); tileReader?.Dispose();
@ -1103,7 +962,6 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
this.FrameDecoder.Dispose();
this.FrameBuffer.Dispose(); this.FrameBuffer.Dispose();
this.TileReader.Dispose(); this.TileReader.Dispose();
} }

20
src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs

@ -271,6 +271,26 @@ internal sealed class Av1FrameBuffer<T> : IDisposable
source.Height, source.Height,
FrameBufferKind.Presentation); FrameBufferKind.Presentation);
/// <summary>
/// Creates a compact monochrome plane for a composed auxiliary image.
/// </summary>
/// <param name="configuration">The configuration providing sample storage.</param>
/// <param name="colorConfig">The sample precision and range of the auxiliary items.</param>
/// <param name="size">The complete auxiliary image extent.</param>
/// <returns>The compact writable auxiliary plane.</returns>
public static Av1FrameBuffer<T> CreateAuxiliary(
Configuration configuration,
ObuColorConfig colorConfig,
Size size)
=> new(
configuration.MemoryAllocator,
new ObuSequenceHeader { ColorConfig = colorConfig },
Av1ColorFormat.Yuv400,
colorConfig.BitDepth > Av1BitDepth.EightBit,
size.Width,
size.Height,
FrameBufferKind.Presentation);
/// <summary> /// <summary>
/// Creates an empty restoration frame with the source's visible dimensions and sample format. /// Creates an empty restoration frame with the source's visible dimensions and sample format.
/// </summary> /// </summary>

190
src/ImageSharp/Formats/Heif/Av1/Color/Av1YuvConverter.cs

@ -4,7 +4,9 @@
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Components; using SixLabors.ImageSharp.Formats.Heif.Components;
using SixLabors.ImageSharp.Formats.Heif.Components.Alpha; using SixLabors.ImageSharp.Formats.Heif.Components.Alpha;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Color; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Color;
@ -15,40 +17,54 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Color;
internal static class Av1YuvConverter internal static class Av1YuvConverter
{ {
/// <summary> /// <summary>
/// Converts the reconstructed component planes to packed pixels. /// Converts a rectangular region of reconstructed component planes directly to packed pixels.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam> /// <typeparam name="TPixel">The destination pixel type.</typeparam>
/// <param name="configuration">The configuration used for allocation and pixel conversion.</param> /// <param name="configuration">The configuration used for allocation and pixel conversion.</param>
/// <param name="frameBuffer">The reconstructed AV1 frame.</param> /// <param name="frameBuffer">The reconstructed AV1 frame.</param>
/// <param name="image">The destination image frame.</param> /// <param name="sourceRectangle">The luma-sample region mapped to the complete destination frame.</param>
public static void ConvertToRgb<TPixel>(Configuration configuration, Av1FrameBuffer<byte> frameBuffer, ImageFrame<TPixel> image) /// <param name="destination">The destination pixel region.</param>
/// <param name="presentationSize">The spatial extent of the presented component planes.</param>
/// <param name="transform">The rotation and mirroring applied within the destination region.</param>
/// <param name="profile">The source profile selected for conversion, or null to preserve source colors.</param>
/// <param name="alphaFrame">The auxiliary plane, or null for opaque pixels.</param>
/// <param name="alphaOutputSize">The complete color extent covered by alpha.</param>
/// <param name="alphaRectangle">The exact matching auxiliary presentation region.</param>
/// <param name="premultiplied">Whether source RGB is associated with alpha.</param>
/// <param name="chromaUpsampling">The chroma reconstruction mode.</param>
/// <param name="isFullRange">Whether RGB conversion interprets the color planes as full-range samples.</param>
public static void ConvertToRgb<TPixel>(
Configuration configuration,
Av1FrameBuffer<byte> frameBuffer,
Rectangle sourceRectangle,
Buffer2DRegion<TPixel> destination,
Size presentationSize,
HeifPixelTransform transform,
IccProfile? profile,
Av1FrameBuffer<byte>? alphaFrame,
Size alphaOutputSize,
Rectangle alphaRectangle,
bool premultiplied,
HeifChromaUpsampling chromaUpsampling,
bool isFullRange)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
HeifColorConversionParameters parameters = GetConversionParameters(frameBuffer.ColorConfig, out HeifColorConversionMode mode); using HeifAlphaRowSource? alpha = alphaFrame is null
? null
: CreateAlphaRowSource(configuration, alphaFrame, alphaOutputSize, alphaRectangle);
HeifColorConversionParameters parameters = GetConversionParameters(frameBuffer.ColorConfig, isFullRange, out HeifColorConversionMode mode);
if (frameBuffer.BitDepth == Av1BitDepth.EightBit) if (frameBuffer.BitDepth == Av1BitDepth.EightBit)
{ {
Av1PlanarSampleBuffer<byte> buffer = new(frameBuffer); Av1PlanarSampleBuffer<byte> buffer = new(frameBuffer);
if (buffer.Width != image.Width || buffer.Height != image.Height) if (presentationSize != new Size(buffer.Width, buffer.Height))
{ {
// AVIF spatial-layer selection scales native YUV planes before color conversion. The retained using Av1PresentationSampleBuffer<byte, Av1PlanarSampleBuffer<byte>> presented = new(
// reconstruction remains untouched because later dependent layers can still reference its coded configuration, buffer, presentationSize.Width, presentationSize.Height);
// dimensions, while this short-lived owner contains only the presented sample grid.
using Av1PresentationSampleBuffer<byte, Av1PlanarSampleBuffer<byte>> presentationBuffer = new(
configuration,
buffer,
image.Width,
image.Height);
HeifPlanarColorConverter.ConvertToRgb< HeifPlanarColorConverter.ConvertToRgb<
TPixel, TPixel, Av1PresentationSampleBufferView<byte, Av1PlanarSampleBuffer<byte>>, byte, HeifByteSampleConverter>(
Av1PresentationSampleBufferView<byte, Av1PlanarSampleBuffer<byte>>, configuration, presented.View, destination, in parameters, mode, sourceRectangle.X, sourceRectangle.Y, sourceRectangle.Size, transform, profile, alpha, premultiplied, chromaUpsampling);
byte,
HeifByteSampleConverter>(
configuration,
presentationBuffer.View,
image,
in parameters,
mode);
return; return;
} }
@ -56,30 +72,29 @@ internal static class Av1YuvConverter
HeifPlanarColorConverter.ConvertToRgb<TPixel, Av1PlanarSampleBuffer<byte>, byte, HeifByteSampleConverter>( HeifPlanarColorConverter.ConvertToRgb<TPixel, Av1PlanarSampleBuffer<byte>, byte, HeifByteSampleConverter>(
configuration, configuration,
buffer, buffer,
image, destination,
in parameters, in parameters,
mode); mode,
sourceRectangle.X,
sourceRectangle.Y,
sourceRectangle.Size,
transform,
profile,
alpha,
premultiplied,
chromaUpsampling);
return; return;
} }
Av1PlanarSampleBuffer<ushort> highBitDepthBuffer = new(frameBuffer); Av1PlanarSampleBuffer<ushort> highBitDepthBuffer = new(frameBuffer);
if (highBitDepthBuffer.Width != image.Width || highBitDepthBuffer.Height != image.Height) if (presentationSize != new Size(highBitDepthBuffer.Width, highBitDepthBuffer.Height))
{ {
using Av1PresentationSampleBuffer<ushort, Av1PlanarSampleBuffer<ushort>> presentationBuffer = new( using Av1PresentationSampleBuffer<ushort, Av1PlanarSampleBuffer<ushort>> presented = new(
configuration, configuration, highBitDepthBuffer, presentationSize.Width, presentationSize.Height);
highBitDepthBuffer,
image.Width,
image.Height);
HeifPlanarColorConverter.ConvertToRgb< HeifPlanarColorConverter.ConvertToRgb<TPixel, Av1PresentationSampleBufferView<ushort, Av1PlanarSampleBuffer<ushort>>>(
TPixel, configuration, presented.View, destination, in parameters, mode, sourceRectangle.X, sourceRectangle.Y, sourceRectangle.Size, transform, profile, alpha, premultiplied, chromaUpsampling);
Av1PresentationSampleBufferView<ushort, Av1PlanarSampleBuffer<ushort>>>(
configuration,
presentationBuffer.View,
image,
in parameters,
mode);
return; return;
} }
@ -87,51 +102,51 @@ internal static class Av1YuvConverter
HeifPlanarColorConverter.ConvertToRgb<TPixel, Av1PlanarSampleBuffer<ushort>>( HeifPlanarColorConverter.ConvertToRgb<TPixel, Av1PlanarSampleBuffer<ushort>>(
configuration, configuration,
highBitDepthBuffer, highBitDepthBuffer,
image, destination,
in parameters, in parameters,
mode); mode,
sourceRectangle.X,
sourceRectangle.Y,
sourceRectangle.Size,
transform,
profile,
alpha,
premultiplied,
chromaUpsampling);
} }
/// <summary> /// <summary>
/// Converts a rectangular region of reconstructed component planes directly to packed pixels. /// Selects a native alpha row reader for the exact color region.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam> /// <param name="configuration">The configuration providing scratch storage.</param>
/// <param name="configuration">The configuration used for allocation and pixel conversion.</param> /// <param name="frame">The native auxiliary samples retained by the caller.</param>
/// <param name="frameBuffer">The reconstructed AV1 frame.</param> /// <param name="outputSize">The complete color extent before cropping and orientation.</param>
/// <param name="sourceRectangle">The luma-sample region mapped to the complete destination frame.</param> /// <param name="window">The color region within that extent.</param>
/// <param name="image">The destination image frame.</param> /// <returns>The row reader whose scratch storage must be disposed after conversion.</returns>
public static void ConvertRegionToRgb<TPixel>( public static HeifAlphaRowSource CreateAlphaRowSource(
Configuration configuration, Configuration configuration,
Av1FrameBuffer<byte> frameBuffer, Av1FrameBuffer<byte> frame,
Rectangle sourceRectangle, Size outputSize,
ImageFrame<TPixel> image) Rectangle window)
where TPixel : unmanaged, IPixel<TPixel>
{ {
HeifColorConversionParameters parameters = GetConversionParameters(frameBuffer.ColorConfig, out HeifColorConversionMode mode); HeifColorConversionParameters parameters = GetConversionParameters(frame.ColorConfig, frame.ColorConfig.ColorRange, out _);
if (frameBuffer.BitDepth == Av1BitDepth.EightBit) Rectangle source = new(0, 0, frame.Width, frame.Height);
if (frame.BitDepth == Av1BitDepth.EightBit)
{ {
Av1PlanarSampleBuffer<byte> buffer = new(frameBuffer); Av1PlanarSampleBuffer<byte> buffer = new(frame);
HeifPlanarColorConverter.ConvertToRgb<TPixel, Av1PlanarSampleBuffer<byte>, byte, HeifByteSampleConverter>( return source.Size == outputSize
configuration, ? new HeifAlphaRowSource<Av1PlanarSampleBuffer<byte>, byte, HeifByteSampleConverter>(
buffer, configuration, buffer, in parameters, window)
image, : new HeifPlanarAlphaResizeWorker<Av1PlanarSampleBuffer<byte>, byte, HeifByteSampleConverter>(
in parameters, configuration, buffer, in parameters, source, window, outputSize);
mode,
sourceRectangle.X,
sourceRectangle.Y);
return;
} }
Av1PlanarSampleBuffer<ushort> highBitDepthBuffer = new(frameBuffer); Av1PlanarSampleBuffer<ushort> highBitDepthBuffer = new(frame);
HeifPlanarColorConverter.ConvertToRgb<TPixel, Av1PlanarSampleBuffer<ushort>>( return source.Size == outputSize
configuration, ? new HeifAlphaRowSource<Av1PlanarSampleBuffer<ushort>, ushort, HeifUShortSampleConverter>(
highBitDepthBuffer, configuration, highBitDepthBuffer, in parameters, window)
image, : new HeifPlanarAlphaResizeWorker<Av1PlanarSampleBuffer<ushort>, ushort, HeifUShortSampleConverter>(
in parameters, configuration, highBitDepthBuffer, in parameters, source, window, outputSize);
mode,
sourceRectangle.X,
sourceRectangle.Y);
} }
/// <summary> /// <summary>
@ -144,13 +159,15 @@ internal static class Av1YuvConverter
/// <param name="outputSize">The complete presented size of the auxiliary image or grid tile.</param> /// <param name="outputSize">The complete presented size of the auxiliary image or grid tile.</param>
/// <param name="destinationRectangle">The destination region receiving the top-left portion of the presented alpha image.</param> /// <param name="destinationRectangle">The destination region receiving the top-left portion of the presented alpha image.</param>
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param> /// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
/// <param name="transform">The rotation and mirroring applied within the destination region.</param>
public static void ComposeAlpha<TPixel>( public static void ComposeAlpha<TPixel>(
Configuration configuration, Configuration configuration,
Av1FrameBuffer<byte> frameBuffer, Av1FrameBuffer<byte> frameBuffer,
ImageFrame<TPixel> destination, Buffer2DRegion<TPixel> destination,
Size outputSize, Size outputSize,
Rectangle destinationRectangle, Rectangle destinationRectangle,
bool premultiplied) bool premultiplied,
HeifPixelTransform transform)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
=> ComposeAlpha( => ComposeAlpha(
configuration, configuration,
@ -159,7 +176,8 @@ internal static class Av1YuvConverter
destination, destination,
outputSize, outputSize,
destinationRectangle, destinationRectangle,
premultiplied); premultiplied,
transform);
/// <summary> /// <summary>
/// Composes a rectangular reconstructed luma region into a packed color frame as auxiliary alpha. /// Composes a rectangular reconstructed luma region into a packed color frame as auxiliary alpha.
@ -172,17 +190,19 @@ internal static class Av1YuvConverter
/// <param name="outputSize">The complete presented size of the auxiliary image or grid tile.</param> /// <param name="outputSize">The complete presented size of the auxiliary image or grid tile.</param>
/// <param name="destinationRectangle">The destination region receiving the presented alpha image.</param> /// <param name="destinationRectangle">The destination region receiving the presented alpha image.</param>
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param> /// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
/// <param name="transform">The rotation and mirroring applied within the destination region.</param>
public static void ComposeAlpha<TPixel>( public static void ComposeAlpha<TPixel>(
Configuration configuration, Configuration configuration,
Av1FrameBuffer<byte> frameBuffer, Av1FrameBuffer<byte> frameBuffer,
Rectangle sourceRectangle, Rectangle sourceRectangle,
ImageFrame<TPixel> destination, Buffer2DRegion<TPixel> destination,
Size outputSize, Size outputSize,
Rectangle destinationRectangle, Rectangle destinationRectangle,
bool premultiplied) bool premultiplied,
HeifPixelTransform transform)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
HeifColorConversionParameters parameters = GetConversionParameters(frameBuffer.ColorConfig, out _); HeifColorConversionParameters parameters = GetConversionParameters(frameBuffer.ColorConfig, frameBuffer.ColorConfig.ColorRange, out _);
if (frameBuffer.BitDepth == Av1BitDepth.EightBit) if (frameBuffer.BitDepth == Av1BitDepth.EightBit)
{ {
Av1PlanarSampleBuffer<byte> buffer = new(frameBuffer); Av1PlanarSampleBuffer<byte> buffer = new(frameBuffer);
@ -194,7 +214,8 @@ internal static class Av1YuvConverter
sourceRectangle, sourceRectangle,
outputSize, outputSize,
destinationRectangle, destinationRectangle,
premultiplied); premultiplied,
transform);
return; return;
} }
@ -208,7 +229,8 @@ internal static class Av1YuvConverter
sourceRectangle, sourceRectangle,
outputSize, outputSize,
destinationRectangle, destinationRectangle,
premultiplied); premultiplied,
transform);
} }
/// <summary> /// <summary>
@ -221,7 +243,7 @@ internal static class Av1YuvConverter
public static void ConvertFromRgb<TPixel>(Configuration configuration, ImageFrame<TPixel> image, Av1FrameBuffer<byte> frameBuffer) public static void ConvertFromRgb<TPixel>(Configuration configuration, ImageFrame<TPixel> image, Av1FrameBuffer<byte> frameBuffer)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
HeifColorConversionParameters parameters = GetConversionParameters(frameBuffer.ColorConfig, out HeifColorConversionMode mode); HeifColorConversionParameters parameters = GetConversionParameters(frameBuffer.ColorConfig, frameBuffer.ColorConfig.ColorRange, out HeifColorConversionMode mode);
if (frameBuffer.BitDepth == Av1BitDepth.EightBit) if (frameBuffer.BitDepth == Av1BitDepth.EightBit)
{ {
Av1PlanarSampleBuffer<byte> buffer = new(frameBuffer); Av1PlanarSampleBuffer<byte> buffer = new(frameBuffer);
@ -248,10 +270,12 @@ internal static class Av1YuvConverter
/// Resolves the H.273 conversion mode, matrix coefficients, and sample range for a frame. /// Resolves the H.273 conversion mode, matrix coefficients, and sample range for a frame.
/// </summary> /// </summary>
/// <param name="colorConfig">The signaled AV1 color configuration.</param> /// <param name="colorConfig">The signaled AV1 color configuration.</param>
/// <param name="isFullRange">Whether conversion interprets the samples as full range.</param>
/// <param name="mode">The resolved conversion mode.</param> /// <param name="mode">The resolved conversion mode.</param>
/// <returns>The resolved conversion parameters.</returns> /// <returns>The resolved conversion parameters.</returns>
public static HeifColorConversionParameters GetConversionParameters( public static HeifColorConversionParameters GetConversionParameters(
ObuColorConfig colorConfig, ObuColorConfig colorConfig,
bool isFullRange,
out HeifColorConversionMode mode) out HeifColorConversionMode mode)
{ {
if (colorConfig.ChromaSamplePosition == ObuChromoSamplePosition.Reserved) if (colorConfig.ChromaSamplePosition == ObuChromoSamplePosition.Reserved)
@ -265,7 +289,7 @@ internal static class Av1YuvConverter
(CicpColorPrimaries)(byte)colorConfig.ColorPrimaries, (CicpColorPrimaries)(byte)colorConfig.ColorPrimaries,
(CicpTransferCharacteristics)(byte)colorConfig.TransferCharacteristics, (CicpTransferCharacteristics)(byte)colorConfig.TransferCharacteristics,
(CicpMatrixCoefficients)(byte)colorConfig.MatrixCoefficients, (CicpMatrixCoefficients)(byte)colorConfig.MatrixCoefficients,
colorConfig.ColorRange, isFullRange,
colorConfig.BitDepth.GetBitCount(), colorConfig.BitDepth.GetBitCount(),
colorConfig.BitDepth.GetBitCount(), colorConfig.BitDepth.GetBitCount(),
isMonochrome, isMonochrome,

42
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs

@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
/// <summary> /// <summary>
/// Reconstructs the coded blocks of one AV1 image frame into planar sample buffers. /// Reconstructs the coded blocks of one AV1 image frame into planar sample buffers.
/// </summary> /// </summary>
internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable internal sealed class Av1FrameDecoder : IAv1FrameDecoder
{ {
/// <summary> /// <summary>
/// The sequence-level superblock and color configuration. /// The sequence-level superblock and color configuration.
@ -42,11 +42,6 @@ internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable
/// </summary> /// </summary>
private readonly Av1ReferenceFrameStore referenceFrames; private readonly Av1ReferenceFrameStore referenceFrames;
/// <summary>
/// The transform-size map populated during reconstruction and consumed by deblocking.
/// </summary>
private readonly Av1LoopFilterContext loopFilterContext;
/// <summary> /// <summary>
/// The block reconstruction stage that applies prediction and inverse transforms. /// The block reconstruction stage that applies prediction and inverse transforms.
/// </summary> /// </summary>
@ -76,31 +71,13 @@ internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable
this.frameInfo = frameInfo; this.frameInfo = frameInfo;
this.frameBuffer = frameBuffer; this.frameBuffer = frameBuffer;
this.referenceFrames = referenceFrames; this.referenceFrames = referenceFrames;
this.loopFilterContext = new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); this.blockDecoder = new(
try this.sequenceHeader,
{ this.frameHeader,
this.blockDecoder = new( this.frameBuffer,
this.sequenceHeader, this.referenceFrames,
this.frameHeader, reconstructionWorkspace,
this.frameBuffer, paletteColorIndexMaps);
this.loopFilterContext,
this.referenceFrames,
reconstructionWorkspace,
paletteColorIndexMaps);
}
catch
{
this.loopFilterContext.Dispose();
throw;
}
}
/// <summary>
/// Releases the pooled block-reconstruction workspaces owned by this decoder.
/// </summary>
public void Dispose()
{
this.loopFilterContext.Dispose();
} }
/// <summary> /// <summary>
@ -120,8 +97,7 @@ internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable
this.sequenceHeader, this.sequenceHeader,
this.frameHeader, this.frameHeader,
this.frameInfo, this.frameInfo,
this.frameBuffer, this.frameBuffer);
this.loopFilterContext);
loopFilterDecoder.DecodeFrame(); loopFilterDecoder.DecodeFrame();

3
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs

@ -1083,6 +1083,7 @@ internal static class Av1FrameEncoder
{ {
HeifColorConversionParameters parameters = Av1YuvConverter.GetConversionParameters( HeifColorConversionParameters parameters = Av1YuvConverter.GetConversionParameters(
colorConfig, colorConfig,
colorConfig.ColorRange,
out HeifColorConversionMode mode); out HeifColorConversionMode mode);
// Conversion writes into the final bordered analysis planes. Later coding stages consume the native // Conversion writes into the final bordered analysis planes. Later coding stages consume the native
@ -1412,7 +1413,7 @@ internal static class Av1FrameEncoder
{ {
// Resolve conversion before renting storage: a rejected color description must not strand an owner // Resolve conversion before renting storage: a rejected color description must not strand an owner
// in a constructor that never returns to the sequence encoder's disposal boundary. // in a constructor that never returns to the sequence encoder's disposal boundary.
this.parameters = Av1YuvConverter.GetConversionParameters(colorConfig, out HeifColorConversionMode mode); this.parameters = Av1YuvConverter.GetConversionParameters(colorConfig, colorConfig.ColorRange, out HeifColorConversionMode mode);
this.colorConverter = HeifColorConverterBase.Create(mode, in this.parameters, colorConfig.IsMonochrome); this.colorConverter = HeifColorConverterBase.Create(mode, in this.parameters, colorConfig.IsMonochrome);
int subsamplingY = colorConfig.SubSamplingY ? 1 : 0; int subsamplingY = colorConfig.SubSamplingY ? 1 : 0;
int componentLength = encodeAlpha int componentLength = encodeAlpha

167
src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterContext.cs

@ -1,167 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter;
/// <summary>
/// Stores the transform-size map consumed by the AV1 deblocking loop filter.
/// </summary>
internal sealed class Av1LoopFilterContext : IDisposable
{
/// <summary>
/// Stores luma transform sizes at plane-relative 4x4 granularity.
/// </summary>
private readonly MemoryGroup<Av1TransformSize> transformSizesY;
/// <summary>
/// The active luma transform-map dimensions in plane-relative 4x4 units.
/// </summary>
private readonly Size transformSizesYSize;
/// <summary>
/// Stores shared-chroma transform sizes at plane-relative 4x4 granularity.
/// </summary>
private readonly MemoryGroup<Av1TransformSize>? transformSizesUv;
/// <summary>
/// The active shared-chroma transform-map dimensions in plane-relative 4x4 units.
/// </summary>
private readonly Size transformSizesUvSize;
/// <summary>
/// Initializes a new instance of the <see cref="Av1LoopFilterContext"/> class.
/// </summary>
/// <param name="memoryAllocator">The allocator that owns the frame-sized transform maps.</param>
/// <param name="sequenceHeader">The sequence header defining superblock and chroma geometry.</param>
/// <param name="frameHeader">The frame header defining active coded dimensions.</param>
public Av1LoopFilterContext(
MemoryAllocator memoryAllocator,
ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader)
{
int modeInfoWidth = frameHeader.ModeInfoColumnCount;
int modeInfoHeight = frameHeader.ModeInfoRowCount;
MemoryGroup<Av1TransformSize>? transformSizesY = null;
MemoryGroup<Av1TransformSize>? transformSizesUv = null;
this.transformSizesUvSize = default;
try
{
long lumaLength = (long)modeInfoWidth * modeInfoHeight;
transformSizesY = memoryAllocator.AllocateGroup<Av1TransformSize>(
lumaLength,
1,
AllocationOptions.Clean);
if (!sequenceHeader.ColorConfig.IsMonochrome)
{
int subX = sequenceHeader.ColorConfig.SubSamplingX ? 1 : 0;
int subY = sequenceHeader.ColorConfig.SubSamplingY ? 1 : 0;
int chromaWidth = Av1Math.DivideLog2Ceiling(modeInfoWidth, subX);
int chromaHeight = Av1Math.DivideLog2Ceiling(modeInfoHeight, subY);
long chromaLength = (long)chromaWidth * chromaHeight;
transformSizesUv = memoryAllocator.AllocateGroup<Av1TransformSize>(
chromaLength,
1,
AllocationOptions.Clean);
this.transformSizesUvSize = new Size(chromaWidth, chromaHeight);
}
this.transformSizesY = transformSizesY;
this.transformSizesYSize = new Size(modeInfoWidth, modeInfoHeight);
this.transformSizesUv = transformSizesUv;
}
catch
{
transformSizesUv?.Dispose();
transformSizesY?.Dispose();
throw;
}
}
/// <summary>
/// Stores a transform size across every 4x4 position covered by one transform block.
/// </summary>
/// <param name="plane">The luma or chroma plane.</param>
/// <param name="position">The transform origin in plane-relative 4x4 units.</param>
/// <param name="transformSize">The transform size.</param>
public void SetTransformSize(Av1Plane plane, Point position, Av1TransformSize transformSize)
{
int planeType = Math.Min((int)plane, (int)Av1PlaneType.Uv);
MemoryGroup<Av1TransformSize> transformSizeMap;
Size transformSizeMapSize;
if (planeType == (int)Av1PlaneType.Y)
{
transformSizeMap = this.transformSizesY;
transformSizeMapSize = this.transformSizesYSize;
}
else
{
transformSizeMap = this.transformSizesUv
?? throw new InvalidOperationException("A monochrome AV1 frame has no chroma transform-size map.");
transformSizeMapSize = this.transformSizesUvSize;
}
int width = Math.Min(transformSize.Get4x4WideCount(), transformSizeMapSize.Width - position.X);
int height = Math.Min(transformSize.Get4x4HighCount(), transformSizeMapSize.Height - position.Y);
// libaom clips transform coverage to the active plane mi dimensions at frame edges. Each logical row may cross
// allocator segments, so fill only the current segment before continuing at the same logical map offset.
for (int y = 0; y < height; y++)
{
long offset = ((long)(position.Y + y) * transformSizeMapSize.Width) + position.X;
int remaining = width;
while (remaining > 0)
{
Span<Av1TransformSize> destination = transformSizeMap.GetRemainingSliceOfBuffer(offset);
int count = Math.Min(remaining, destination.Length);
destination[..count].Fill(transformSize);
offset += count;
remaining -= count;
}
}
}
/// <summary>
/// Gets the transform size covering a plane-relative 4x4 position.
/// </summary>
/// <param name="plane">The luma or chroma plane.</param>
/// <param name="position">The position in plane-relative 4x4 units.</param>
/// <returns>The transform size covering the position.</returns>
public Av1TransformSize GetTransformSize(Av1Plane plane, Point position)
{
int planeType = Math.Min((int)plane, (int)Av1PlaneType.Uv);
MemoryGroup<Av1TransformSize> transformSizeMap;
int width;
if (planeType == (int)Av1PlaneType.Y)
{
transformSizeMap = this.transformSizesY;
width = this.transformSizesYSize.Width;
}
else
{
transformSizeMap = this.transformSizesUv
?? throw new InvalidOperationException("A monochrome AV1 frame has no chroma transform-size map.");
width = this.transformSizesUvSize.Width;
}
long offset = ((long)position.Y * width) + position.X;
return transformSizeMap.GetRemainingSliceOfBuffer(offset)[0];
}
/// <summary>
/// Returns the allocator-owned transform-size maps.
/// </summary>
public void Dispose()
{
this.transformSizesUv?.Dispose();
this.transformSizesY.Dispose();
}
}

26
src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterDecoder.cs

@ -34,11 +34,6 @@ internal sealed class Av1LoopFilterDecoder
/// </summary> /// </summary>
private readonly Av1FrameBuffer<byte> frameBuffer; private readonly Av1FrameBuffer<byte> frameBuffer;
/// <summary>
/// The per-plane transform-size map populated during reconstruction.
/// </summary>
private readonly Av1LoopFilterContext loopFilterContext;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Av1LoopFilterDecoder"/> class. /// Initializes a new instance of the <see cref="Av1LoopFilterDecoder"/> class.
/// </summary> /// </summary>
@ -46,19 +41,16 @@ internal sealed class Av1LoopFilterDecoder
/// <param name="frameHeader">The frame header defining dimensions and filter parameters.</param> /// <param name="frameHeader">The frame header defining dimensions and filter parameters.</param>
/// <param name="frameInfo">The decoded block-mode and superblock delta information.</param> /// <param name="frameInfo">The decoded block-mode and superblock delta information.</param>
/// <param name="frameBuffer">The reconstructed frame samples to filter.</param> /// <param name="frameBuffer">The reconstructed frame samples to filter.</param>
/// <param name="loopFilterContext">The transform-size map populated during reconstruction.</param>
public Av1LoopFilterDecoder( public Av1LoopFilterDecoder(
ObuSequenceHeader sequenceHeader, ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader, ObuFrameHeader frameHeader,
Av1FrameInfo frameInfo, Av1FrameInfo frameInfo,
Av1FrameBuffer<byte> frameBuffer, Av1FrameBuffer<byte> frameBuffer)
Av1LoopFilterContext loopFilterContext)
{ {
this.sequenceHeader = sequenceHeader; this.sequenceHeader = sequenceHeader;
this.frameHeader = frameHeader; this.frameHeader = frameHeader;
this.frameInfo = frameInfo; this.frameInfo = frameInfo;
this.frameBuffer = frameBuffer; this.frameBuffer = frameBuffer;
this.loopFilterContext = loopFilterContext;
} }
/// <summary> /// <summary>
@ -155,7 +147,7 @@ internal sealed class Av1LoopFilterDecoder
} }
/// <summary> /// <summary>
/// Reads deblocking parameters from decoded modes and the reconstructed transform map. /// Reads deblocking parameters from the decoded block metadata.
/// </summary> /// </summary>
private readonly struct FrameOperator : Av1LoopFilterBase.IFrameOperator<Av1LoopFilterDecoder, Av1BlockModeInfo> private readonly struct FrameOperator : Av1LoopFilterBase.IFrameOperator<Av1LoopFilterDecoder, Av1BlockModeInfo>
{ {
@ -175,7 +167,19 @@ internal sealed class Av1LoopFilterDecoder
mode = state.frameInfo.GetModeInfoAt(position); mode = state.frameInfo.GetModeInfoAt(position);
blockIndex = mode.ModeInfoIndex; blockIndex = mode.ModeInfoIndex;
skippedTransform = mode.Skip && mode.ReferenceFrames[0] > Av1ReferenceFrameType.Intra; skippedTransform = mode.Skip && mode.ReferenceFrames[0] > Av1ReferenceFrameType.Intra;
transformSize = state.loopFilterContext.GetTransformSize(plane, new Point(position.X >> subX, position.Y >> subY)); transformSize = state.frameHeader.LosslessArray[mode.SegmentId]
? Av1TransformSize.Size4x4
: plane == Av1Plane.Y
? mode.TransformSize
: mode.BlockSize.GetMaxUvTransformSize(subX != 0, subY != 0);
if (plane == Av1Plane.Y && mode.ReferenceFrames[0] > Av1ReferenceFrameType.Intra && !mode.Skip &&
!state.frameHeader.LosslessArray[mode.SegmentId])
{
int row = position.Y & (mode.BlockSize.Get4x4HighCount() - 1);
int column = position.X & (mode.BlockSize.Get4x4WideCount() - 1);
transformSize = mode.InterTransformSizes[mode.GetInterTransformSizeIndex(row, column)];
}
} }
/// <inheritdoc/> /// <inheritdoc/>

34
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs

@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -19,6 +20,11 @@ internal struct Av1BlockModeInfo
/// </summary> /// </summary>
private InlineArray2<Av1ReferenceFrameType> referenceFrames; private InlineArray2<Av1ReferenceFrameType> referenceFrames;
/// <summary>
/// Stores variable luma transform sizes on the coding block's compact transform grid.
/// </summary>
private InlineArray16<Av1TransformSize> interTransformSizes;
/// <summary> /// <summary>
/// Stores the motion vector associated with each reference-frame label. /// Stores the motion vector associated with each reference-frame label.
/// </summary> /// </summary>
@ -120,6 +126,17 @@ internal struct Av1BlockModeInfo
/// </summary> /// </summary>
public int ModeInfoIndex { get; set; } public int ModeInfoIndex { get; set; }
/// <summary>
/// Gets or sets the selected luma transform size.
/// </summary>
public Av1TransformSize TransformSize { get; set; }
/// <summary>
/// Gets the variable luma transform sizes retained with this coding block.
/// </summary>
[UnscopedRef]
public Span<Av1TransformSize> InterTransformSizes => this.interTransformSizes;
/// <summary> /// <summary>
/// Gets or sets the <see cref="Av1PredictionMode"/> for the luminance channel. /// Gets or sets the <see cref="Av1PredictionMode"/> for the luminance channel.
/// </summary> /// </summary>
@ -281,6 +298,23 @@ internal struct Av1BlockModeInfo
/// </summary> /// </summary>
public Av1FilterIntraMode FilterIntraMode { get; set; } public Av1FilterIntraMode FilterIntraMode { get; set; }
/// <summary>
/// Finds the compact transform-grid entry covering a position relative to the coding block.
/// </summary>
/// <param name="row">The row in luma 4x4 units.</param>
/// <param name="column">The column in luma 4x4 units.</param>
/// <returns>The entry index in the block's sixteen-element transform grid.</returns>
public int GetInterTransformSizeIndex(int row, int column)
{
// One subdivision of the maximum transform defines the storage cell. A final split into
// 4x4 transforms shares one entry because all children of that split have the same size.
Av1TransformSize cellSize = this.BlockSize.GetMaximumTransformSize().GetSubSize();
int cellWidth = cellSize.Get4x4WideCount();
int cellHeight = cellSize.Get4x4HighCount();
int stride = this.BlockSize.Get4x4WideCount() / cellWidth;
return ((row / cellHeight) * stride) + (column / cellWidth);
}
/// <summary> /// <summary>
/// Gets the directional prediction angle adjustment for a color plane. /// Gets the directional prediction angle adjustment for a color plane.
/// </summary> /// </summary>

21
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs

@ -1639,7 +1639,8 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
if (usesInterTransformSyntax && if (usesInterTransformSyntax &&
!modeInfo.Skip && !modeInfo.Skip &&
this.FrameHeader.TransformMode == Av1TransformMode.Select && this.FrameHeader.TransformMode == Av1TransformMode.Select &&
blockSize > Av1BlockSize.Block4x4) blockSize > Av1BlockSize.Block4x4 &&
!this.FrameHeader.LosslessArray[modeInfo.SegmentId])
{ {
this.ReadVariableTransformInfo( this.ReadVariableTransformInfo(
ref reader, ref reader,
@ -1661,6 +1662,12 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
tileInfo, tileInfo,
allowSelect); allowSelect);
modeInfo.TransformSize = transformSize;
if (usesInterTransformSyntax)
{
modeInfo.InterTransformSizes.Fill(transformSize);
}
bool skippedInterBlock = usesInterTransformSyntax && modeInfo.Skip; bool skippedInterBlock = usesInterTransformSyntax && modeInfo.Skip;
this.aboveNeighborContext.UpdateTransformation(modeInfoLocation, tileInfo, transformSize, blockSize, skippedInterBlock); this.aboveNeighborContext.UpdateTransformation(modeInfoLocation, tileInfo, transformSize, blockSize, skippedInterBlock);
this.leftNeighborContext.UpdateTransformation(modeInfoLocation, superblockInfo, transformSize, blockSize, skippedInterBlock); this.leftNeighborContext.UpdateTransformation(modeInfoLocation, superblockInfo, transformSize, blockSize, skippedInterBlock);
@ -1804,6 +1811,18 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
return; return;
} }
ref Av1BlockModeInfo modeInfo = ref partitionInfo.ModeInfo;
modeInfo.TransformSize = transformSize;
Av1TransformSize cellSize = blockSize.GetMaximumTransformSize().GetSubSize();
for (int row = 0; row < transformSize.Get4x4HighCount(); row += cellSize.Get4x4HighCount())
{
for (int column = 0; column < transformSize.Get4x4WideCount(); column += cellSize.Get4x4WideCount())
{
int index = modeInfo.GetInterTransformSizeIndex(blockRow + row, blockColumn + column);
modeInfo.InterTransformSizes[index] = transformSize;
}
}
Span<Av1TransformInfo> transformInfo = superblockInfo.GetTransformInfoY(); Span<Av1TransformInfo> transformInfo = superblockInfo.GetTransformInfoY();
transformInfo[transformInfoIndex] = new Av1TransformInfo(transformSize, blockColumn, blockRow); transformInfo[transformInfoIndex] = new Av1TransformInfo(transformSize, blockColumn, blockRow);
transformInfoIndex++; transformInfoIndex++;

30
src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs

@ -4,7 +4,6 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
@ -34,11 +33,6 @@ internal sealed class Av1BlockDecoder
/// </summary> /// </summary>
private readonly Av1FrameBuffer<byte> frameBuffer; private readonly Av1FrameBuffer<byte> frameBuffer;
/// <summary>
/// The per-plane transform-size map consumed after reconstruction by the deblocking stage.
/// </summary>
private readonly Av1LoopFilterContext loopFilterContext;
/// <summary> /// <summary>
/// The retained reconstructed frames addressable by inter prediction. /// The retained reconstructed frames addressable by inter prediction.
/// </summary> /// </summary>
@ -64,11 +58,6 @@ internal sealed class Av1BlockDecoder
/// </summary> /// </summary>
private readonly Av1PredictionDecoder predictionDecoder; private readonly Av1PredictionDecoder predictionDecoder;
/// <summary>
/// Indicates whether transform traversal must also populate loop-filter parameters.
/// </summary>
private readonly bool isLoopFilterEnabled;
/// <summary> /// <summary>
/// The next raster coefficient region for each plane in the current superblock. /// The next raster coefficient region for each plane in the current superblock.
/// </summary> /// </summary>
@ -85,7 +74,6 @@ internal sealed class Av1BlockDecoder
/// <param name="sequenceHeader">The decoded sequence header.</param> /// <param name="sequenceHeader">The decoded sequence header.</param>
/// <param name="frameHeader">The decoded frame header.</param> /// <param name="frameHeader">The decoded frame header.</param>
/// <param name="frameBuffer">The frame buffer receiving reconstructed samples.</param> /// <param name="frameBuffer">The frame buffer receiving reconstructed samples.</param>
/// <param name="loopFilterContext">The transform-size map populated while reconstructing blocks.</param>
/// <param name="referenceFrames">The retained reconstructed frames selected by inter blocks.</param> /// <param name="referenceFrames">The retained reconstructed frames selected by inter blocks.</param>
/// <param name="workspace">The reconstruction storage available for the lifetime of this frame.</param> /// <param name="workspace">The reconstruction storage available for the lifetime of this frame.</param>
/// <param name="paletteColorIndexMaps">The complete decoder-session palette map state.</param> /// <param name="paletteColorIndexMaps">The complete decoder-session palette map state.</param>
@ -93,7 +81,6 @@ internal sealed class Av1BlockDecoder
ObuSequenceHeader sequenceHeader, ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader, ObuFrameHeader frameHeader,
Av1FrameBuffer<byte> frameBuffer, Av1FrameBuffer<byte> frameBuffer,
Av1LoopFilterContext loopFilterContext,
Av1ReferenceFrameStore referenceFrames, Av1ReferenceFrameStore referenceFrames,
Memory<short> workspace, Memory<short> workspace,
Av1TileReader.PaletteColorIndexMaps? paletteColorIndexMaps = null) Av1TileReader.PaletteColorIndexMaps? paletteColorIndexMaps = null)
@ -101,7 +88,6 @@ internal sealed class Av1BlockDecoder
this.sequenceHeader = sequenceHeader; this.sequenceHeader = sequenceHeader;
this.frameHeader = frameHeader; this.frameHeader = frameHeader;
this.frameBuffer = frameBuffer; this.frameBuffer = frameBuffer;
this.loopFilterContext = loopFilterContext;
this.referenceFrames = referenceFrames; this.referenceFrames = referenceFrames;
this.workspace = workspace; this.workspace = workspace;
int maximumBlockLength = 1 << sequenceHeader.SuperblockSizeLog2; int maximumBlockLength = 1 << sequenceHeader.SuperblockSizeLog2;
@ -122,8 +108,6 @@ internal sealed class Av1BlockDecoder
frameHeader, frameHeader,
predictionScratch.Slice(predictorWorkingOffset, predictorWorkingLength), predictionScratch.Slice(predictorWorkingOffset, predictorWorkingLength),
paletteColorIndexMaps); paletteColorIndexMaps);
this.isLoopFilterEnabled = frameHeader.LoopFilterParameters.FilterLevel[0] != 0 ||
frameHeader.LoopFilterParameters.FilterLevel[1] != 0;
this.chromaFromLumaContext = new( this.chromaFromLumaContext = new(
sequenceHeader.ColorConfig, sequenceHeader.ColorConfig,
@ -1357,20 +1341,6 @@ internal sealed class Av1BlockDecoder
transformBlockReconstructionBuffer = blockReconstructionBuffer[transformBlockOffset..]; transformBlockReconstructionBuffer = blockReconstructionBuffer[transformBlockOffset..];
} }
if (this.isLoopFilterEnabled)
{
// U and V share transform geometry. Store the chroma map once so the later plane passes consume
// identical sizes without retaining duplicate state.
if (plane != 2)
{
Point transformPosition = new(
(modeInfoPosition.X >> subX) + transformInfo.OffsetX,
(modeInfoPosition.Y >> subY) + transformInfo.OffsetY);
this.loopFilterContext.SetTransformSize((Av1Plane)plane, transformPosition, transformSize);
}
}
if (!isInterBlock && !modeInfo.UseIntraBlockCopy) if (!isInterBlock && !modeInfo.UseIntraBlockCopy)
{ {
// Conventional intra prediction consumes the reference-prefixed destination span before the // Conventional intra prediction consumes the reference-prefixed destination span before the

85
src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs

@ -2,8 +2,12 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Color;
using SixLabors.ImageSharp.Formats.Heif.Components.Alpha; using SixLabors.ImageSharp.Formats.Heif.Components.Alpha;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Heif; namespace SixLabors.ImageSharp.Formats.Heif;
@ -12,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Heif;
/// Decodes a single AV1-coded HEIF image item. /// Decodes a single AV1-coded HEIF image item.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam> /// <typeparam name="TPixel">The destination pixel type.</typeparam>
internal sealed class Av1HeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>, IHeifAlphaItemDecoder<TPixel> internal sealed class Av1HeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>, IHeifAlphaItemDecoder
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
/// <summary> /// <summary>
@ -20,27 +24,41 @@ internal sealed class Av1HeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>, IHe
/// </summary> /// </summary>
public Heif4CharCode Type => Heif4CharCode.Av01; public Heif4CharCode Type => Heif4CharCode.Av01;
/// <summary>
/// Gets the AV1 compression method.
/// </summary>
public HeifCompressionMethod CompressionMethod => HeifCompressionMethod.Av1;
/// <summary> /// <summary>
/// Decodes the encoded AV1 payload of an image item. /// Decodes the encoded AV1 payload of an image item.
/// </summary> /// </summary>
/// <param name="options">The general options governing the containing HEIF decode.</param> /// <param name="options">The general options governing the containing HEIF decode.</param>
/// <param name="chromaUpsampling">The chroma reconstruction mode.</param>
/// <param name="item">The HEIF item whose encoded payload is being decoded.</param> /// <param name="item">The HEIF item whose encoded payload is being decoded.</param>
/// <param name="data">The encoded AV1 payload.</param> /// <param name="data">The encoded AV1 payload.</param>
/// <param name="colorProfile"> /// <param name="colorProfile">
/// The container color description that supplies unspecified color information in the AV1 sequence header. /// The container color description that supplies unspecified color information in the AV1 sequence header.
/// </param> /// </param>
/// <param name="profile">The source ICC profile selected for conversion, or null to preserve source colors.</param>
/// <param name="alphaFrame">The native auxiliary plane, or null for an opaque image.</param>
/// <param name="alphaOutputSize">The complete color extent covered by the auxiliary plane.</param>
/// <param name="alphaRectangle">The matching region within the auxiliary presentation.</param>
/// <param name="premultiplied">Whether source RGB is associated with alpha.</param>
/// <param name="sourceRectangle">The source area of interest in luma-sample coordinates.</param>
/// <param name="transform">The rotation and mirroring applied within the destination region.</param>
/// <param name="destination">The destination pixel region.</param>
/// <param name="metadata">The metadata receiving the decoded image properties.</param>
/// <param name="cancellationToken">The token used to cancel the payload decode.</param> /// <param name="cancellationToken">The token used to cancel the payload decode.</param>
/// <returns>The decoded image.</returns> public void DecodeItemData(
public Image<TPixel> DecodeItemData(
DecoderOptions options, DecoderOptions options,
HeifChromaUpsampling chromaUpsampling,
HeifItem item, HeifItem item,
Span<byte> data, Span<byte> data,
CicpProfile? colorProfile, CicpProfile? colorProfile,
IccProfile? profile,
Av1FrameBuffer<byte>? alphaFrame,
Size alphaOutputSize,
Rectangle alphaRectangle,
bool premultiplied,
Rectangle sourceRectangle,
HeifPixelTransform transform,
Buffer2DRegion<TPixel> destination,
ImageMetadata metadata,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@ -55,31 +73,42 @@ internal sealed class Av1HeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>, IHe
byte operatingPointIndex = item.Av1OperatingPointSelector?.Index ?? 0; byte operatingPointIndex = item.Av1OperatingPointSelector?.Index ?? 0;
using Av1Decoder decoder = new(options.Configuration, operatingPointIndex); using Av1Decoder decoder = new(options.Configuration, operatingPointIndex);
Image<TPixel> image = decoder.Decode<TPixel>( using Av1FrameBuffer<byte> frameBuffer = decoder.DecodeFrameBuffer(
itemData, itemData,
colorProfile, colorProfile,
codecConfiguration, codecConfiguration,
item.Av1LayeredImageIndex, out CicpProfile effectiveColorProfile,
item.Extent); item.Av1LayeredImageIndex);
HeifMetadata metadata = image.Metadata.GetHeifMetadata(); // Container range describes presentation; the bitstream range remains attached to the decoded planes.
metadata.CompressionMethod = this.CompressionMethod; Av1YuvConverter.ConvertToRgb(
metadata.BitDepth = codecConfiguration.BitDepth; options.Configuration,
metadata.IsMonochrome = codecConfiguration.IsMonochrome; frameBuffer,
metadata.ContentLightLevel = item.ContentLightLevel ?? obuContentLightLevel; sourceRectangle,
metadata.MasteringDisplayColorVolume = item.MasteringDisplayColorVolume ?? obuMasteringDisplayColorVolume; destination,
return image; item.Extent,
transform,
profile,
alphaFrame,
alphaOutputSize,
alphaRectangle,
premultiplied,
chromaUpsampling,
colorProfile?.FullRange ?? frameBuffer.ColorConfig.ColorRange);
metadata.CicpProfile = effectiveColorProfile;
HeifMetadata heifMetadata = metadata.GetHeifMetadata();
heifMetadata.BitDepth = codecConfiguration.BitDepth;
heifMetadata.IsMonochrome = codecConfiguration.IsMonochrome;
heifMetadata.ContentLightLevel = item.ContentLightLevel ?? obuContentLightLevel;
heifMetadata.MasteringDisplayColorVolume = item.MasteringDisplayColorVolume ?? obuMasteringDisplayColorVolume;
} }
/// <inheritdoc/> /// <inheritdoc/>
public void DecodeAlphaItemData( public Av1FrameBuffer<byte> DecodeAlphaItemData(
DecoderOptions options, DecoderOptions options,
HeifItem item, HeifItem item,
Span<byte> data, Span<byte> data,
ImageFrame<TPixel> destination,
Size outputSize,
Rectangle destinationRectangle,
bool premultiplied,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@ -93,15 +122,11 @@ internal sealed class Av1HeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>, IHe
byte operatingPointIndex = item.Av1OperatingPointSelector?.Index ?? 0; byte operatingPointIndex = item.Av1OperatingPointSelector?.Index ?? 0;
using Av1Decoder decoder = new(options.Configuration, operatingPointIndex); using Av1Decoder decoder = new(options.Configuration, operatingPointIndex);
decoder.DecodeAlpha( return decoder.DecodeFrameBuffer(
itemData, itemData,
item.CicpProfile, item.CicpProfile,
codecConfiguration, codecConfiguration,
default, out _,
destination,
outputSize,
destinationRectangle,
premultiplied,
item.Av1LayeredImageIndex); item.Av1LayeredImageIndex);
} }

74
src/ImageSharp/Formats/Heif/Components/Alpha/HeifAlphaRowSource.cs

@ -0,0 +1,74 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Components.Alpha;
/// <summary>
/// Supplies normalized auxiliary samples for the exact color region being converted.
/// </summary>
internal abstract class HeifAlphaRowSource : IDisposable
{
/// <summary>
/// Reads the next row in increasing source-row order.
/// </summary>
/// <param name="y">The row relative to the selected color region.</param>
/// <returns>The normalized samples, valid until the next row is read.</returns>
public abstract Span<float> ReadRow(int y);
/// <inheritdoc/>
public abstract void Dispose();
}
/// <summary>
/// Reads or resamples native auxiliary samples without packing them into image pixels.
/// </summary>
/// <typeparam name="TBuffer">The native component-plane view.</typeparam>
/// <typeparam name="TSample">The native unsigned sample type.</typeparam>
/// <typeparam name="TLoader">The sample widening operator.</typeparam>
internal sealed class HeifAlphaRowSource<TBuffer, TSample, TLoader> : HeifAlphaRowSource
where TBuffer : struct, IHeifPlanarSampleBuffer<TSample>
where TSample : unmanaged
where TLoader : struct, IHeifSampleConverter<TSample>
{
private readonly TBuffer buffer;
private readonly HeifColorConversionParameters parameters;
private readonly Rectangle window;
private readonly IMemoryOwner<float> rowOwner;
/// <summary>
/// Initializes a new instance of the <see cref="HeifAlphaRowSource{TBuffer, TSample, TLoader}"/> class.
/// </summary>
/// <param name="configuration">The configuration providing scratch storage.</param>
/// <param name="buffer">The native auxiliary plane retained by the decoder.</param>
/// <param name="parameters">The auxiliary sample range.</param>
/// <param name="window">The exact color region within that extent.</param>
public HeifAlphaRowSource(
Configuration configuration,
TBuffer buffer,
in HeifColorConversionParameters parameters,
Rectangle window)
{
this.buffer = buffer;
this.parameters = parameters;
this.window = window;
this.rowOwner = configuration.MemoryAllocator.Allocate<float>(window.Width);
}
/// <inheritdoc/>
public override Span<float> ReadRow(int y)
{
Span<float> row = this.rowOwner.GetSpan()[..this.window.Width];
ReadOnlySpan<TSample> source = this.buffer.GetLumaRowSpan(this.window.Y + y).Slice(this.window.X, this.window.Width);
HeifPlanarAlphaCompositor.NormalizeAlphaRow<TSample, TLoader>(source, row, in this.parameters);
return row;
}
/// <inheritdoc/>
public override void Dispose()
{
this.rowOwner.Dispose();
}
}

91
src/ImageSharp/Formats/Heif/Components/Alpha/HeifPlanarAlphaCompositor.cs

@ -2,11 +2,11 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers; using System.Buffers;
using System.Numerics;
using System.Numerics.Tensors; using System.Numerics.Tensors;
using SixLabors.ImageSharp.Formats.Heif.Components; using SixLabors.ImageSharp.Formats.Heif.Components;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
namespace SixLabors.ImageSharp.Formats.Heif.Components.Alpha; namespace SixLabors.ImageSharp.Formats.Heif.Components.Alpha;
@ -30,15 +30,17 @@ internal static class HeifPlanarAlphaCompositor
/// <param name="outputSize">The complete presented size of the auxiliary image or grid tile.</param> /// <param name="outputSize">The complete presented size of the auxiliary image or grid tile.</param>
/// <param name="destinationRectangle">The destination region receiving the top-left portion of the presented alpha image.</param> /// <param name="destinationRectangle">The destination region receiving the top-left portion of the presented alpha image.</param>
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param> /// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
/// <param name="transform">The rotation and mirroring applied within the destination region.</param>
public static void Compose<TPixel, TBuffer, TSample, TLoader>( public static void Compose<TPixel, TBuffer, TSample, TLoader>(
Configuration configuration, Configuration configuration,
TBuffer buffer, TBuffer buffer,
ImageFrame<TPixel> destination, Buffer2DRegion<TPixel> destination,
in HeifColorConversionParameters parameters, in HeifColorConversionParameters parameters,
Rectangle sourceRectangle, Rectangle sourceRectangle,
Size outputSize, Size outputSize,
Rectangle destinationRectangle, Rectangle destinationRectangle,
bool premultiplied) bool premultiplied,
HeifPixelTransform transform)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
where TBuffer : struct, IHeifPlanarSampleBuffer<TSample> where TBuffer : struct, IHeifPlanarSampleBuffer<TSample>
where TSample : unmanaged where TSample : unmanaged
@ -50,6 +52,7 @@ internal static class HeifPlanarAlphaCompositor
int outputHeight = outputSize.Height; int outputHeight = outputSize.Height;
int composedWidth = destinationRectangle.Width; int composedWidth = destinationRectangle.Width;
int composedHeight = destinationRectangle.Height; int composedHeight = destinationRectangle.Height;
Matrix3x2 matrix = transform.GetMatrix(destinationRectangle.Size);
if (sourceWidth == outputWidth && sourceHeight == outputHeight) if (sourceWidth == outputWidth && sourceHeight == outputHeight)
{ {
@ -64,32 +67,34 @@ internal static class HeifPlanarAlphaCompositor
// destination row. No resize maps or full-plane staging are required. // destination row. No resize maps or full-plane staging are required.
for (int y = 0; y < composedHeight; y++) for (int y = 0; y < composedHeight; y++)
{ {
ReadOnlySpan<TSample> source = buffer.GetLumaRowSpan(sourceRectangle.Y + y).Slice(sourceRectangle.X, composedWidth); ReadOnlySpan<TSample> source = buffer.GetLumaRowSpan(sourceRectangle.Y + destinationRectangle.Y + y)
.Slice(sourceRectangle.X + destinationRectangle.X, composedWidth);
NormalizeAlphaRow<TSample, TLoader>(source, alpha, in parameters); NormalizeAlphaRow<TSample, TLoader>(source, alpha, in parameters);
ApplyAlphaRow(configuration, destination, destinationRectangle.X, destinationRectangle.Y + y, alpha, packedAlpha, packedColor, premultiplied); ApplyAlphaRow(configuration, destination, y, alpha, packedAlpha, packedColor, premultiplied, matrix);
} }
return; return;
} }
// Alpha scaling must match KnownResamplers.Box. That public instance is exposed as IResampler, while using HeifPlanarAlphaResizeWorker<TBuffer, TSample, TLoader> worker = new(
// ResizeKernelMap requires the concrete struct so Radius and GetValue remain statically dispatched.
// BoxResampler is stateless, making its default value behaviorally identical to the known instance.
BoxResampler boxResampler = default;
using ResizeKernelMap horizontalKernels = ResizeKernelMap.Calculate(in boxResampler, outputWidth, sourceWidth, configuration.MemoryAllocator);
using ResizeKernelMap verticalKernels = ResizeKernelMap.Calculate(in boxResampler, outputHeight, sourceHeight, configuration.MemoryAllocator);
using HeifPlanarAlphaResizeWorker<TPixel, TBuffer, TSample, TLoader> worker = new(
configuration, configuration,
buffer, buffer,
destination,
in parameters, in parameters,
sourceRectangle, sourceRectangle,
destinationRectangle, destinationRectangle,
horizontalKernels, outputSize);
verticalKernels,
premultiplied); using IMemoryOwner<L16> resizedAlphaOwner = configuration.MemoryAllocator.Allocate<L16>(composedWidth);
using IMemoryOwner<Rgba64> resizedColorOwner = configuration.MemoryAllocator.Allocate<Rgba64>(composedWidth);
Span<L16> resizedPackedAlpha = resizedAlphaOwner.GetSpan()[..composedWidth];
Span<Rgba64> resizedPackedColor = resizedColorOwner.GetSpan()[..composedWidth];
worker.Compose(); for (int y = 0; y < composedHeight; y++)
{
ApplyAlphaRow(
configuration, destination, y, worker.ReadRow(y), resizedPackedAlpha, resizedPackedColor, premultiplied, matrix);
}
} }
/// <summary> /// <summary>
@ -107,13 +112,10 @@ internal static class HeifPlanarAlphaCompositor
where TSample : unmanaged where TSample : unmanaged
where TLoader : struct, IHeifSampleConverter<TSample> where TLoader : struct, IHeifSampleConverter<TSample>
{ {
HeifSampleConversion.ConvertSamplesToFloat<TSample, TLoader>(source, destination); HeifSampleConversion.ConvertSamplesToFloat<TSample, TLoader>(source, destination, parameters.LumaBias, parameters.LumaScale);
// Alpha auxiliaries use the luma code-value range but no color matrix. TensorPrimitives keeps this bulk // Normalization divides by the encoded range during widening. Multiplication by a rounded
// normalization SIMD-first on every supported architecture and clamps before resampling, matching the // reciprocal can move alpha across a final half-unit boundary. Clamp before resampling.
// established conversion to a bounded L16 plane.
TensorPrimitives.Subtract(destination, parameters.LumaBias, destination);
TensorPrimitives.Multiply(destination, 1F / parameters.LumaScale, destination);
TensorPrimitives.Clamp(destination, 0F, 1F, destination); TensorPrimitives.Clamp(destination, 0F, 1F, destination);
} }
@ -123,29 +125,44 @@ internal static class HeifPlanarAlphaCompositor
/// <typeparam name="TPixel">The destination pixel type.</typeparam> /// <typeparam name="TPixel">The destination pixel type.</typeparam>
/// <param name="configuration">The configuration used for pixel conversion.</param> /// <param name="configuration">The configuration used for pixel conversion.</param>
/// <param name="destination">The packed destination frame receiving alpha values.</param> /// <param name="destination">The packed destination frame receiving alpha values.</param>
/// <param name="destinationX">The horizontal start of the destination region.</param> /// <param name="destinationY">The source row mapped into the destination region.</param>
/// <param name="destinationY">The destination row receiving alpha values.</param>
/// <param name="alpha">The normalized alpha samples.</param> /// <param name="alpha">The normalized alpha samples.</param>
/// <param name="packedAlpha">The reusable 16-bit alpha packing row.</param> /// <param name="packedAlpha">The reusable 16-bit alpha packing row.</param>
/// <param name="packedColor">The reusable high-bit-depth destination color row.</param> /// <param name="packedColor">The reusable high-bit-depth destination color row.</param>
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param> /// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
/// <param name="matrix">The matrix resolved once for the destination region.</param>
public static void ApplyAlphaRow<TPixel>( public static void ApplyAlphaRow<TPixel>(
Configuration configuration, Configuration configuration,
ImageFrame<TPixel> destination, Buffer2DRegion<TPixel> destination,
int destinationX,
int destinationY, int destinationY,
ReadOnlySpan<float> alpha, ReadOnlySpan<float> alpha,
Span<L16> packedAlpha, Span<L16> packedAlpha,
Span<Rgba64> packedColor, Span<Rgba64> packedColor,
bool premultiplied) bool premultiplied,
Matrix3x2 matrix)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int width = alpha.Length; int width = alpha.Length;
Span<TPixel> destinationRow = destination.PixelBuffer.DangerousGetRowSpan(destinationY).Slice(destinationX, width); Point rowStart = HeifPixelTransform.Transform(0, destinationY, matrix);
Size rowStep = new((int)matrix.M11, (int)matrix.M12);
PixelOperations<TPixel> pixelOperations = PixelOperations<TPixel>.Instance; PixelOperations<TPixel> pixelOperations = PixelOperations<TPixel>.Instance;
HeifSampleConversion.PackL16(alpha, packedAlpha); HeifSampleConversion.PackL16(alpha, packedAlpha);
pixelOperations.ToRgba64(configuration, destinationRow, packedColor); if (matrix.IsIdentity)
{
pixelOperations.ToRgba64(configuration, destination.DangerousGetRowSpan(destinationY), packedColor);
}
else
{
// Gather only this alpha row's color pixels from their final coordinates. No second image is needed.
Point point = rowStart;
for (int x = 0; x < width; x++)
{
packedColor[x] = Rgba64.FromScaledVector4(destination.DangerousGetRowSpan(point.Y)[point.X].ToScaledVector4());
point += rowStep;
}
}
if (premultiplied) if (premultiplied)
{ {
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
@ -168,6 +185,18 @@ internal static class HeifPlanarAlphaCompositor
} }
} }
pixelOperations.FromRgba64(configuration, packedColor, destinationRow); if (matrix.IsIdentity)
{
pixelOperations.FromRgba64(configuration, packedColor, destination.DangerousGetRowSpan(destinationY));
}
else
{
Point point = rowStart;
for (int x = 0; x < width; x++)
{
destination.DangerousGetRowSpan(point.Y)[point.X] = TPixel.FromRgba64(packedColor[x]);
point += rowStep;
}
}
} }
} }

126
src/ImageSharp/Formats/Heif/Components/Alpha/HeifPlanarAlphaResizeWorker.cs

@ -14,12 +14,10 @@ namespace SixLabors.ImageSharp.Formats.Heif.Components.Alpha;
/// <summary> /// <summary>
/// Resizes a native HEIF luma plane and composes the result as alpha using a bounded sliding window. /// Resizes a native HEIF luma plane and composes the result as alpha using a bounded sliding window.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
/// <typeparam name="TBuffer">The codec adapter exposing the reconstructed component planes.</typeparam> /// <typeparam name="TBuffer">The codec adapter exposing the reconstructed component planes.</typeparam>
/// <typeparam name="TSample">The native unsigned sample storage type.</typeparam> /// <typeparam name="TSample">The native unsigned sample storage type.</typeparam>
/// <typeparam name="TLoader">The SIMD widening operations for the sample type.</typeparam> /// <typeparam name="TLoader">The SIMD widening operations for the sample type.</typeparam>
internal sealed class HeifPlanarAlphaResizeWorker<TPixel, TBuffer, TSample, TLoader> : IDisposable internal sealed class HeifPlanarAlphaResizeWorker<TBuffer, TSample, TLoader> : HeifAlphaRowSource
where TPixel : unmanaged, IPixel<TPixel>
where TBuffer : struct, IHeifPlanarSampleBuffer<TSample> where TBuffer : struct, IHeifPlanarSampleBuffer<TSample>
where TSample : unmanaged where TSample : unmanaged
where TLoader : struct, IHeifSampleConverter<TSample> where TLoader : struct, IHeifSampleConverter<TSample>
@ -34,11 +32,6 @@ internal sealed class HeifPlanarAlphaResizeWorker<TPixel, TBuffer, TSample, TLoa
/// </summary> /// </summary>
private readonly TBuffer buffer; private readonly TBuffer buffer;
/// <summary>
/// The packed color frame receiving alpha values.
/// </summary>
private readonly ImageFrame<TPixel> destination;
/// <summary> /// <summary>
/// The resolved H.273 component-range parameters. /// The resolved H.273 component-range parameters.
/// </summary> /// </summary>
@ -84,16 +77,6 @@ internal sealed class HeifPlanarAlphaResizeWorker<TPixel, TBuffer, TSample, TLoa
/// </summary> /// </summary>
private readonly IMemoryOwner<L16> alphaOwner; private readonly IMemoryOwner<L16> alphaOwner;
/// <summary>
/// The reusable high-bit-depth destination color row.
/// </summary>
private readonly IMemoryOwner<Rgba64> colorOwner;
/// <summary>
/// Whether stored color samples must be converted to unassociated alpha.
/// </summary>
private readonly bool premultiplied;
/// <summary> /// <summary>
/// The number of source rows retained when the window advances. /// The number of source rows retained when the window advances.
/// </summary> /// </summary>
@ -110,38 +93,31 @@ internal sealed class HeifPlanarAlphaResizeWorker<TPixel, TBuffer, TSample, TLoa
private RowInterval currentWindow; private RowInterval currentWindow;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="HeifPlanarAlphaResizeWorker{TPixel, TBuffer, TSample, TLoader}"/> class. /// Initializes a new instance of the <see cref="HeifPlanarAlphaResizeWorker{TBuffer, TSample, TLoader}"/> class.
/// </summary> /// </summary>
/// <param name="configuration">The configuration used for pooled allocation and pixel conversion.</param> /// <param name="configuration">The configuration used for pooled allocation and pixel conversion.</param>
/// <param name="buffer">The codec-native component planes.</param> /// <param name="buffer">The codec-native component planes.</param>
/// <param name="destination">The packed color frame receiving alpha values.</param>
/// <param name="parameters">The resolved H.273 component-range parameters.</param> /// <param name="parameters">The resolved H.273 component-range parameters.</param>
/// <param name="sourceRectangle">The visible luma rectangle within the reconstructed plane.</param> /// <param name="sourceRectangle">The visible luma rectangle within the reconstructed plane.</param>
/// <param name="destinationRectangle">The destination region receiving the top-left portion of the presented alpha plane.</param> /// <param name="destinationRectangle">The destination region receiving the top-left portion of the presented alpha plane.</param>
/// <param name="horizontalKernels">The horizontal box-filter kernels for the full presented width.</param> /// <param name="outputSize">The complete presentation extent before cropping.</param>
/// <param name="verticalKernels">The vertical box-filter kernels for the full presented height.</param>
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
public HeifPlanarAlphaResizeWorker( public HeifPlanarAlphaResizeWorker(
Configuration configuration, Configuration configuration,
TBuffer buffer, TBuffer buffer,
ImageFrame<TPixel> destination,
in HeifColorConversionParameters parameters, in HeifColorConversionParameters parameters,
Rectangle sourceRectangle, Rectangle sourceRectangle,
Rectangle destinationRectangle, Rectangle destinationRectangle,
ResizeKernelMap horizontalKernels, Size outputSize)
ResizeKernelMap verticalKernels,
bool premultiplied)
{ {
this.configuration = configuration; this.configuration = configuration;
this.buffer = buffer; this.buffer = buffer;
this.destination = destination;
this.parameters = parameters; this.parameters = parameters;
this.sourceRectangle = sourceRectangle; this.sourceRectangle = sourceRectangle;
this.destinationRectangle = destinationRectangle; this.destinationRectangle = destinationRectangle;
this.premultiplied = premultiplied;
this.horizontalKernels = horizontalKernels; BoxResampler resampler = default;
this.verticalKernels = verticalKernels; this.horizontalKernels = ResizeKernelMap.Calculate(in resampler, outputSize.Width, sourceRectangle.Width, configuration.MemoryAllocator);
this.verticalKernels = ResizeKernelMap.Calculate(in resampler, outputSize.Height, sourceRectangle.Height, configuration.MemoryAllocator);
// Retaining one complete maximum-diameter band is sufficient for every vertical kernel that crosses a // Retaining one complete maximum-diameter band is sufficient for every vertical kernel that crosses a
// window boundary. Those first-pass rows can be copied forward instead of normalized and filtered again. // window boundary. Those first-pass rows can be copied forward instead of normalized and filtered again.
@ -170,35 +146,32 @@ internal sealed class HeifPlanarAlphaResizeWorker<TPixel, TBuffer, TSample, TLoa
this.componentOwner = configuration.MemoryAllocator.Allocate<float>(Math.Max(sourceRectangle.Width, destinationRectangle.Width)); this.componentOwner = configuration.MemoryAllocator.Allocate<float>(Math.Max(sourceRectangle.Width, destinationRectangle.Width));
this.sourceVectorOwner = configuration.MemoryAllocator.Allocate<Vector4>(sourceRectangle.Width); this.sourceVectorOwner = configuration.MemoryAllocator.Allocate<Vector4>(sourceRectangle.Width);
this.alphaOwner = configuration.MemoryAllocator.Allocate<L16>(Math.Max(sourceRectangle.Width, destinationRectangle.Width)); this.alphaOwner = configuration.MemoryAllocator.Allocate<L16>(Math.Max(sourceRectangle.Width, destinationRectangle.Width));
this.colorOwner = configuration.MemoryAllocator.Allocate<Rgba64>(destinationRectangle.Width);
this.currentWindow = new RowInterval(0, this.workerHeight); this.currentWindow = new RowInterval(0, this.workerHeight);
this.CalculateFirstPassValues(this.currentWindow);
} }
/// <summary> /// <summary>
/// Releases all allocator-owned working buffers. /// Releases all allocator-owned working buffers.
/// </summary> /// </summary>
public void Dispose() public override void Dispose()
{ {
this.transposedFirstPassBuffer.Dispose(); this.transposedFirstPassBuffer.Dispose();
this.componentOwner.Dispose(); this.componentOwner.Dispose();
this.sourceVectorOwner.Dispose(); this.sourceVectorOwner.Dispose();
this.alphaOwner.Dispose(); this.alphaOwner.Dispose();
this.colorOwner.Dispose(); this.horizontalKernels.Dispose();
this.verticalKernels.Dispose();
} }
/// <summary> /// <summary>
/// Resizes and composes the complete requested destination rectangle. /// Resizes the next row of the requested destination rectangle.
/// </summary> /// </summary>
public void Compose() /// <param name="y">The next destination row, in increasing order starting at zero.</param>
/// <returns>The normalized row, valid until the next row is read.</returns>
public override Span<float> ReadRow(int y)
{ {
// Populate the horizontal first pass for the initial bounded source-row interval. Later windows retain their
// overlap and calculate only newly entering rows.
this.CalculateFirstPassValues(this.currentWindow);
Span<Vector4> transposed = this.transposedFirstPassBuffer.DangerousGetSingleSpan(); Span<Vector4> transposed = this.transposedFirstPassBuffer.DangerousGetSingleSpan();
Span<float> resizedAlpha = this.componentOwner.GetSpan()[..this.destinationRectangle.Width]; Span<float> resizedAlpha = this.componentOwner.GetSpan()[..this.destinationRectangle.Width];
Span<L16> packedAlpha = this.alphaOwner.GetSpan()[..this.destinationRectangle.Width];
Span<Rgba64> packedColor = this.colorOwner.GetSpan()[..this.destinationRectangle.Width];
ReadOnlySpan<ResizeKernel> verticalKernelSpan = this.verticalKernels.GetKernelSpan(); ReadOnlySpan<ResizeKernel> verticalKernelSpan = this.verticalKernels.GetKernelSpan();
ref ResizeKernel verticalKernelBase = ref MemoryMarshal.GetReference(verticalKernelSpan); ref ResizeKernel verticalKernelBase = ref MemoryMarshal.GetReference(verticalKernelSpan);
ref float resizedAlphaBase = ref MemoryMarshal.GetReference(resizedAlpha); ref float resizedAlphaBase = ref MemoryMarshal.GetReference(resizedAlpha);
@ -208,47 +181,36 @@ internal sealed class HeifPlanarAlphaResizeWorker<TPixel, TBuffer, TSample, TLoa
nuint workerHeight = (uint)this.workerHeight; nuint workerHeight = (uint)this.workerHeight;
nuint twoWorkerHeights = workerHeight * 2; nuint twoWorkerHeights = workerHeight * 2;
for (int y = 0; y < this.destinationRectangle.Height; y++) ref ResizeKernel kernel = ref Unsafe.Add(ref verticalKernelBase, this.destinationRectangle.Y + y);
{ int kernelEnd = kernel.StartIndex + kernel.Length;
ref ResizeKernel kernel = ref Unsafe.Add(ref verticalKernelBase, y);
int kernelEnd = kernel.StartIndex + kernel.Length;
// Destination kernels advance monotonically through source Y. Slide until the complete kernel lies in
// the cached first-pass interval; the retained overlap prevents any shared source row being recalculated.
while (kernelEnd > currentWindowMax)
{
this.Slide();
currentWindowMin = this.currentWindow.Min;
currentWindowMax = this.currentWindow.Max;
}
// Values for one destination X are contiguous along source Y in the transposed buffer. ConvolveCore // Destination kernels advance monotonically through source Y. Slide until the complete kernel lies in
// therefore reads the vertical kernel without gathers, while workerHeight advances to the next X column. // the cached first-pass interval; the retained overlap prevents any shared source row being recalculated.
ref Vector4 column = ref transposed[kernel.StartIndex - currentWindowMin]; while (kernelEnd > currentWindowMax)
nuint x = 0; {
for (; x + 1 < width; x += 2) this.Slide();
{ currentWindowMin = this.currentWindow.Min;
Unsafe.Add(ref resizedAlphaBase, x) = kernel.ConvolveCore(ref column).X; currentWindowMax = this.currentWindow.Max;
ref Vector4 nextColumn = ref Unsafe.Add(ref column, workerHeight); }
Unsafe.Add(ref resizedAlphaBase, x + 1) = kernel.ConvolveCore(ref nextColumn).X;
column = ref Unsafe.Add(ref column, twoWorkerHeights);
}
if (x < width) // Values for one destination X are contiguous along source Y in the transposed buffer. ConvolveCore
{ // therefore reads the vertical kernel without gathers, while workerHeight advances to the next X column.
Unsafe.Add(ref resizedAlphaBase, x) = kernel.ConvolveCore(ref column).X; ref Vector4 column = ref transposed[kernel.StartIndex - currentWindowMin];
} nuint x = 0;
for (; x + 1 < width; x += 2)
{
Unsafe.Add(ref resizedAlphaBase, x) = kernel.ConvolveCore(ref column).X;
ref Vector4 nextColumn = ref Unsafe.Add(ref column, workerHeight);
Unsafe.Add(ref resizedAlphaBase, x + 1) = kernel.ConvolveCore(ref nextColumn).X;
column = ref Unsafe.Add(ref column, twoWorkerHeights);
}
HeifPlanarAlphaCompositor.ApplyAlphaRow( if (x < width)
this.configuration, {
this.destination, Unsafe.Add(ref resizedAlphaBase, x) = kernel.ConvolveCore(ref column).X;
this.destinationRectangle.X,
this.destinationRectangle.Y + y,
resizedAlpha,
packedAlpha,
packedColor,
this.premultiplied);
} }
return resizedAlpha;
} }
/// <summary> /// <summary>
@ -307,15 +269,15 @@ internal sealed class HeifPlanarAlphaResizeWorker<TPixel, TBuffer, TSample, TLoa
int x = 0; int x = 0;
for (; x + 1 < destinationWidth; x += 2) for (; x + 1 < destinationWidth; x += 2)
{ {
ref ResizeKernel kernel0 = ref Unsafe.Add(ref horizontalKernelBase, x); ref ResizeKernel kernel0 = ref Unsafe.Add(ref horizontalKernelBase, this.destinationRectangle.X + x);
ref ResizeKernel kernel1 = ref Unsafe.Add(ref horizontalKernelBase, x + 1); ref ResizeKernel kernel1 = ref Unsafe.Add(ref horizontalKernelBase, this.destinationRectangle.X + x + 1);
Unsafe.Add(ref firstPass, (nuint)x * workerHeight) = kernel0.Convolve(sourceVectors); Unsafe.Add(ref firstPass, (nuint)x * workerHeight) = kernel0.Convolve(sourceVectors);
Unsafe.Add(ref firstPass, (nuint)(x + 1) * workerHeight) = kernel1.Convolve(sourceVectors); Unsafe.Add(ref firstPass, (nuint)(x + 1) * workerHeight) = kernel1.Convolve(sourceVectors);
} }
if (x < destinationWidth) if (x < destinationWidth)
{ {
ref ResizeKernel kernel = ref Unsafe.Add(ref horizontalKernelBase, x); ref ResizeKernel kernel = ref Unsafe.Add(ref horizontalKernelBase, this.destinationRectangle.X + x);
Unsafe.Add(ref firstPass, (nuint)x * workerHeight) = kernel.Convolve(sourceVectors); Unsafe.Add(ref firstPass, (nuint)x * workerHeight) = kernel.Convolve(sourceVectors);
} }
} }

27
src/ImageSharp/Formats/Heif/Components/Alpha/IHeifAlphaItemDecoder.cs

@ -1,35 +1,26 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Formats.Heif.Av1;
namespace SixLabors.ImageSharp.Formats.Heif.Components.Alpha; namespace SixLabors.ImageSharp.Formats.Heif.Components.Alpha;
/// <summary> /// <summary>
/// Decodes one coded HEIF auxiliary alpha item directly into a packed color frame. /// Decodes an auxiliary item into native samples for joint color and alpha conversion.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The destination color pixel type.</typeparam> internal interface IHeifAlphaItemDecoder
internal interface IHeifAlphaItemDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{ {
/// <summary> /// <summary>
/// Decodes and composes one coded auxiliary alpha item. /// Decodes the auxiliary item and transfers its native sample plane to the caller.
/// </summary> /// </summary>
/// <param name="options">The general options governing the containing HEIF decode.</param> /// <param name="options">The options governing payload validation and allocation.</param>
/// <param name="item">The auxiliary image item whose encoded payload is being decoded.</param> /// <param name="item">The auxiliary image item.</param>
/// <param name="data">The encoded auxiliary payload.</param> /// <param name="data">The encoded auxiliary payload.</param>
/// <param name="destination">The packed color frame receiving alpha values.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="outputSize">The complete presented size of the auxiliary image or grid tile.</param> /// <returns>The native plane owned by the caller.</returns>
/// <param name="destinationRectangle">The destination region receiving the top-left portion of the presented alpha image.</param> public Av1FrameBuffer<byte> DecodeAlphaItemData(
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
/// <param name="cancellationToken">The token used to cancel the payload decode.</param>
public void DecodeAlphaItemData(
DecoderOptions options, DecoderOptions options,
HeifItem item, HeifItem item,
Span<byte> data, Span<byte> data,
ImageFrame<TPixel> destination,
Size outputSize,
Rectangle destinationRectangle,
bool premultiplied,
CancellationToken cancellationToken); CancellationToken cancellationToken);
} }

100
src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.Alpha.cs

@ -0,0 +1,100 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Components;
internal abstract partial class HeifColorConverterBase
{
/// <summary>
/// Unassociates normalized RGB components before color-profile conversion.
/// </summary>
/// <param name="red">The associated red components.</param>
/// <param name="green">The associated green components.</param>
/// <param name="blue">The associated blue components.</param>
/// <param name="alpha">The normalized auxiliary samples.</param>
public static void UnassociateRgb(Span<float> red, Span<float> green, Span<float> blue, ReadOnlySpan<float> alpha)
{
ref float r = ref MemoryMarshal.GetReference(red);
ref float g = ref MemoryMarshal.GetReference(green);
ref float b = ref MemoryMarshal.GetReference(blue);
ref float a = ref MemoryMarshal.GetReference(alpha);
int x = 0;
// Each lane is one pixel, with the same lane index in all four planes. Divide the
// three color vectors by the alpha vector directly; no interleaving or lane broadcast
// is needed. Zero alpha preserves hidden RGB, matching the shared unassociation contract.
if (Vector512.IsHardwareAccelerated)
{
for (; x <= red.Length - Vector512<float>.Count; x += Vector512<float>.Count)
{
Vector512<float> av = Vector512.LoadUnsafe(ref a, (nuint)x);
Vector512<float> rv = Vector512.LoadUnsafe(ref r, (nuint)x);
Vector512<float> gv = Vector512.LoadUnsafe(ref g, (nuint)x);
Vector512<float> bv = Vector512.LoadUnsafe(ref b, (nuint)x);
Vector512<float> zeroAlpha = Vector512.Equals(av, Vector512<float>.Zero);
rv = Vector512.ConditionalSelect(zeroAlpha, rv, rv / av);
gv = Vector512.ConditionalSelect(zeroAlpha, gv, gv / av);
bv = Vector512.ConditionalSelect(zeroAlpha, bv, bv / av);
Vector512.Clamp(rv, Vector512<float>.Zero, Vector512.Create(1F)).StoreUnsafe(ref r, (nuint)x);
Vector512.Clamp(gv, Vector512<float>.Zero, Vector512.Create(1F)).StoreUnsafe(ref g, (nuint)x);
Vector512.Clamp(bv, Vector512<float>.Zero, Vector512.Create(1F)).StoreUnsafe(ref b, (nuint)x);
}
}
if (Vector256.IsHardwareAccelerated)
{
for (; x <= red.Length - Vector256<float>.Count; x += Vector256<float>.Count)
{
Vector256<float> av = Vector256.LoadUnsafe(ref a, (nuint)x);
Vector256<float> rv = Vector256.LoadUnsafe(ref r, (nuint)x);
Vector256<float> gv = Vector256.LoadUnsafe(ref g, (nuint)x);
Vector256<float> bv = Vector256.LoadUnsafe(ref b, (nuint)x);
Vector256<float> zeroAlpha = Vector256.Equals(av, Vector256<float>.Zero);
rv = Vector256.ConditionalSelect(zeroAlpha, rv, rv / av);
gv = Vector256.ConditionalSelect(zeroAlpha, gv, gv / av);
bv = Vector256.ConditionalSelect(zeroAlpha, bv, bv / av);
Vector256.Clamp(rv, Vector256<float>.Zero, Vector256.Create(1F)).StoreUnsafe(ref r, (nuint)x);
Vector256.Clamp(gv, Vector256<float>.Zero, Vector256.Create(1F)).StoreUnsafe(ref g, (nuint)x);
Vector256.Clamp(bv, Vector256<float>.Zero, Vector256.Create(1F)).StoreUnsafe(ref b, (nuint)x);
}
}
if (Vector128.IsHardwareAccelerated)
{
for (; x <= red.Length - Vector128<float>.Count; x += Vector128<float>.Count)
{
Vector128<float> av = Vector128.LoadUnsafe(ref a, (nuint)x);
Vector128<float> rv = Vector128.LoadUnsafe(ref r, (nuint)x);
Vector128<float> gv = Vector128.LoadUnsafe(ref g, (nuint)x);
Vector128<float> bv = Vector128.LoadUnsafe(ref b, (nuint)x);
Vector128<float> zeroAlpha = Vector128.Equals(av, Vector128<float>.Zero);
rv = Vector128.ConditionalSelect(zeroAlpha, rv, rv / av);
gv = Vector128.ConditionalSelect(zeroAlpha, gv, gv / av);
bv = Vector128.ConditionalSelect(zeroAlpha, bv, bv / av);
Vector128.Clamp(rv, Vector128<float>.Zero, Vector128.Create(1F)).StoreUnsafe(ref r, (nuint)x);
Vector128.Clamp(gv, Vector128<float>.Zero, Vector128.Create(1F)).StoreUnsafe(ref g, (nuint)x);
Vector128.Clamp(bv, Vector128<float>.Zero, Vector128.Create(1F)).StoreUnsafe(ref b, (nuint)x);
}
}
// Successively narrower vectors consume their own tails. The remaining zero to three
// pixels use the same division and clamp without touching the auxiliary plane.
for (; x < red.Length; x++)
{
float value = alpha[x];
if (value != 0F)
{
red[x] /= value;
green[x] /= value;
blue[x] /= value;
}
red[x] = Math.Clamp(red[x], 0F, 1F);
green[x] = Math.Clamp(green[x], 0F, 1F);
blue[x] = Math.Clamp(blue[x], 0F, 1F);
}
}
}

235
src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.Icc.cs

@ -0,0 +1,235 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Numerics.Tensors;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.ColorProfiles;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp.Formats.Heif.Components;
internal abstract partial class HeifColorConverterBase
{
/// <summary>
/// Converts normalized unassociated source components to sRGB before pixel packing.
/// </summary>
/// <param name="red">The normalized red or monochrome component row.</param>
/// <param name="green">The normalized green component row.</param>
/// <param name="blue">The normalized blue component row.</param>
/// <param name="converter">The profile converter selected for this image region.</param>
/// <param name="packed">The reusable interleaved RGB row.</param>
public void ConvertRgbToSrgbInPlace(
Span<float> red,
Span<float> green,
Span<float> blue,
ColorProfileConverter converter,
Span<Rgb> packed)
{
// A monochrome profile consumes one normalized channel. Color profiles consume the
// RGB produced by the signaled H.273 operator, not the encoded YUV components.
if (this.IsMonochrome)
{
// Limited-range samples can reconstruct outside the nominal interval. ICC device
// curves address that interval, so clip before looking up their transfer functions.
TensorPrimitives.Clamp(red, 0F, 1F, red);
converter.Convert<Y, Rgb>(MemoryMarshal.Cast<float, Y>(red), packed);
}
else
{
Interleave3(red, green, blue, MemoryMarshal.Cast<Rgb, float>(packed));
converter.Convert<Rgb, Rgb>(packed, packed);
}
UnpackDeinterleave3(MemoryMarshal.Cast<Rgb, Vector3>(packed), red, green, blue);
}
/// <summary>
/// Interleaves three planar component lanes into packed XYZ values.
/// </summary>
/// <param name="xLane">The planar X components.</param>
/// <param name="yLane">The planar Y components.</param>
/// <param name="zLane">The planar Z components.</param>
/// <param name="packed">The destination ordered as consecutive XYZ triples.</param>
private static void Interleave3(
ReadOnlySpan<float> xLane,
ReadOnlySpan<float> yLane,
ReadOnlySpan<float> zLane,
Span<float> packed)
{
DebugGuard.IsTrue(packed.Length % 3 == 0, "Packed length must be divisible by 3.");
DebugGuard.IsTrue(yLane.Length == xLane.Length, nameof(yLane), "Channels must be of same size!");
DebugGuard.IsTrue(zLane.Length == xLane.Length, nameof(zLane), "Channels must be of same size!");
DebugGuard.MustBeLessThanOrEqualTo(packed.Length / 3, xLane.Length, nameof(packed));
ref float xLaneRef = ref MemoryMarshal.GetReference(xLane);
ref float yLaneRef = ref MemoryMarshal.GetReference(yLane);
ref float zLaneRef = ref MemoryMarshal.GetReference(zLane);
ref float packedRef = ref MemoryMarshal.GetReference(packed);
int i = 0;
if (Vector128.IsHardwareAccelerated)
{
int oneVectorFromEnd = xLane.Length - Vector128<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count)
{
// Each source vector contains four consecutive samples from one plane:
// x = [X0 X1 X2 X3]
// y = [Y0 Y1 Y2 Y3]
// z = [Z0 Z1 Z2 Z3]
// Shifting X by one sample supplies the value that follows each XYZ triple:
// shiftedX = [X1 X2 X3 0]
// The transpose therefore produces overlapping rows [Xn Yn Zn Xn+1].
// AlignRight joins those rows into three complete destination vectors, avoiding
// the scalar-sized stores that writing four independent Vector3 values requires.
Vector128<float> x = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref xLaneRef, i));
Vector128<float> y = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref yLaneRef, i));
Vector128<float> z = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref zLaneRef, i));
// YUV reconstruction can overshoot the device RGB range. Clip while loading
// the planes, before ICC curve lookup, without quantizing through packed pixels.
x = Vector128.Clamp(x, Vector128<float>.Zero, Vector128.Create(1F));
y = Vector128.Clamp(y, Vector128<float>.Zero, Vector128.Create(1F));
z = Vector128.Clamp(z, Vector128<float>.Zero, Vector128.Create(1F));
Vector128<float> shiftedX = Vector128_.ShiftRightBytesInVector(x.AsByte(), sizeof(float)).AsSingle();
Transpose4(
x,
y,
z,
shiftedX,
out Vector128<float> pixel0,
out Vector128<float> pixel1,
out Vector128<float> pixel2,
out Vector128<float> pixel3);
// Dropping pixel2.X lets [Y2] complete [Y1 Z1 X2] from pixel1.
Vector128<byte> shiftedPixel2 = Vector128_.ShiftRightBytesInVector(pixel2.AsByte(), sizeof(float));
Vector128<float> packed1 = Vector128_.AlignRight(shiftedPixel2, pixel1.AsByte(), sizeof(float)).AsSingle();
// Dropping pixel3.X leaves [Y3 Z3] to complete [Z2 X3] from pixel2.
Vector128<byte> shiftedPixel3 = Vector128_.ShiftRightBytesInVector(pixel3.AsByte(), sizeof(float));
Vector128<float> packed2 = Vector128_.AlignRight(shiftedPixel3, pixel2.AsByte(), sizeof(float) * 2).AsSingle();
ref Vector128<float> destination = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref packedRef, (uint)i * 3));
destination = pixel0;
Unsafe.Add(ref destination, 1) = packed1;
Unsafe.Add(ref destination, 2) = packed2;
}
}
// Fewer than four pixels remain after SIMD, or every pixel reaches this path
// when the runtime cannot accelerate the cross-vector transpose.
for (; i < xLane.Length; i++)
{
nuint sourceOffset = (uint)i;
nuint packedOffset = sourceOffset * 3;
Unsafe.Add(ref packedRef, packedOffset) = Math.Clamp(Unsafe.Add(ref xLaneRef, sourceOffset), 0F, 1F);
Unsafe.Add(ref packedRef, packedOffset + 1) = Math.Clamp(Unsafe.Add(ref yLaneRef, sourceOffset), 0F, 1F);
Unsafe.Add(ref packedRef, packedOffset + 2) = Math.Clamp(Unsafe.Add(ref zLaneRef, sourceOffset), 0F, 1F);
}
}
/// <summary>
/// Deinterleaves packed XYZ values into three planar component lanes.
/// </summary>
/// <param name="packed">The source ordered as consecutive XYZ triples.</param>
/// <param name="xLane">The destination X components.</param>
/// <param name="yLane">The destination Y components.</param>
/// <param name="zLane">The destination Z components.</param>
private static void UnpackDeinterleave3(ReadOnlySpan<Vector3> packed, Span<float> xLane, Span<float> yLane, Span<float> zLane)
{
DebugGuard.IsTrue(packed.Length == xLane.Length, nameof(packed), "Channels must be of same size!");
DebugGuard.IsTrue(yLane.Length == xLane.Length, nameof(yLane), "Channels must be of same size!");
DebugGuard.IsTrue(zLane.Length == xLane.Length, nameof(zLane), "Channels must be of same size!");
ref float packedRef = ref MemoryMarshal.GetReference(MemoryMarshal.Cast<Vector3, float>(packed));
ref float xLaneRef = ref MemoryMarshal.GetReference(xLane);
ref float yLaneRef = ref MemoryMarshal.GetReference(yLane);
ref float zLaneRef = ref MemoryMarshal.GetReference(zLane);
int i = 0;
if (Vector128.IsHardwareAccelerated)
{
int oneVectorFromEnd = packed.Length - Vector128<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count)
{
// A Vector3 occupies twelve contiguous bytes, so a sixteen-byte load beginning
// at one pixel also reads the X component of the following pixel:
// pixel0 = [X0 Y0 Z0 X1]
// pixel1 = [X1 Y1 Z1 X2]
// The transpose discards this fourth column, making the overlap useful padding
// and avoiding two insert instructions per pixel. The final row needs explicit
// zero padding only when pixel3 is the last element in the source span.
nuint packedOffset = (uint)i * 3;
Vector128<float> pixel0 = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref packedRef, packedOffset));
Vector128<float> pixel1 = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref packedRef, packedOffset + 3));
Vector128<float> pixel2 = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref packedRef, packedOffset + 6));
ref float pixel3Ref = ref Unsafe.Add(ref packedRef, packedOffset + 9);
Vector128<float> pixel3 = i + Vector128<float>.Count < packed.Length
? Unsafe.As<float, Vector128<float>>(ref pixel3Ref)
: Unsafe.As<float, Vector3>(ref pixel3Ref).AsVector128();
Transpose4(pixel0, pixel1, pixel2, pixel3, out Vector128<float> x, out Vector128<float> y, out Vector128<float> z, out _);
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref xLaneRef, i)) = x;
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref yLaneRef, i)) = y;
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref zLaneRef, i)) = z;
}
}
// The scalar remainder preserves the original scatter behavior for zero to
// three pixels and provides the complete fallback on unsupported hardware.
for (; i < packed.Length; i++)
{
nuint packedOffset = (uint)i * 3;
Unsafe.Add(ref xLaneRef, i) = Unsafe.Add(ref packedRef, packedOffset);
Unsafe.Add(ref yLaneRef, i) = Unsafe.Add(ref packedRef, packedOffset + 1);
Unsafe.Add(ref zLaneRef, i) = Unsafe.Add(ref packedRef, packedOffset + 2);
}
}
/// <summary>
/// Transposes four four-lane rows into four four-lane columns.
/// </summary>
/// <param name="row0">The first matrix row.</param>
/// <param name="row1">The second matrix row.</param>
/// <param name="row2">The third matrix row.</param>
/// <param name="row3">The fourth matrix row.</param>
/// <param name="column0">The first matrix column.</param>
/// <param name="column1">The second matrix column.</param>
/// <param name="column2">The third matrix column.</param>
/// <param name="column3">The fourth matrix column.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Transpose4(
Vector128<float> row0,
Vector128<float> row1,
Vector128<float> row2,
Vector128<float> row3,
out Vector128<float> column0,
out Vector128<float> column1,
out Vector128<float> column2,
out Vector128<float> column3)
{
// The first unpack interleaves adjacent 32-bit lanes from rows 0/1 and 2/3:
// row01Low = [r0c0 r1c0 r0c1 r1c1]
// row23Low = [r2c0 r3c0 r2c1 r3c1]
// A second unpack treats each adjacent pair as one 64-bit lane and combines
// the row01 and row23 pairs into complete columns. The integer views only
// expose the cross-platform unpack helpers; every floating-point bit is preserved.
Vector128<int> row01Low = Vector128_.UnpackLow(row0.AsInt32(), row1.AsInt32());
Vector128<int> row01High = Vector128_.UnpackHigh(row0.AsInt32(), row1.AsInt32());
Vector128<int> row23Low = Vector128_.UnpackLow(row2.AsInt32(), row3.AsInt32());
Vector128<int> row23High = Vector128_.UnpackHigh(row2.AsInt32(), row3.AsInt32());
column0 = Vector128_.UnpackLow(row01Low.AsInt64(), row23Low.AsInt64()).AsSingle();
column1 = Vector128_.UnpackHigh(row01Low.AsInt64(), row23Low.AsInt64()).AsSingle();
column2 = Vector128_.UnpackLow(row01High.AsInt64(), row23High.AsInt64()).AsSingle();
column3 = Vector128_.UnpackHigh(row01High.AsInt64(), row23High.AsInt64()).AsSingle();
}
}

56
src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.Operator.cs

@ -185,7 +185,7 @@ internal abstract partial class HeifColorConverterBase
{ {
HeifColorConversionParameters parameters = this.Parameters; HeifColorConversionParameters parameters = this.Parameters;
// Row reconstruction owns equally sized planar buffers. As in JPEG, first-element byrefs let each // Row reconstruction owns equally sized planar buffers. First-element byrefs let each
// SIMD width share one offset while the closed operator type keeps color-model dispatch out of the loop. // SIMD width share one offset while the closed operator type keeps color-model dispatch out of the loop.
ref float component0Base = ref MemoryMarshal.GetReference(component0); ref float component0Base = ref MemoryMarshal.GetReference(component0);
ref float component1Base = ref MemoryMarshal.GetReference(component1); ref float component1Base = ref MemoryMarshal.GetReference(component1);
@ -195,16 +195,14 @@ internal abstract partial class HeifColorConverterBase
if (this.IsMonochrome) if (this.IsMonochrome)
{ {
// Monochrome has no operator arithmetic: expanding the luma range once and copying each SIMD // Monochrome is already normalized by the sample loader. Copying each SIMD
// vector to all three planes is cheaper than routing it through a three-component operator. // vector to all three planes is cheaper than routing it through a three-component operator.
if (Vector512.IsHardwareAccelerated && i <= length - Vector512<float>.Count) if (Vector512.IsHardwareAccelerated && i <= length - Vector512<float>.Count)
{ {
Vector512<float> bias = Vector512.Create(parameters.LumaBias);
Vector512<float> scale = Vector512.Create(parameters.LumaScale);
int oneVectorFromEnd = length - Vector512<float>.Count; int oneVectorFromEnd = length - Vector512<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<float>.Count) for (; i <= oneVectorFromEnd; i += Vector512<float>.Count)
{ {
Vector512<float> value = (Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component0Base, i)) - bias) / scale; Vector512<float> value = Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component0Base, i));
Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component0Base, i)) = value; Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component0Base, i)) = value;
Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component1Base, i)) = value; Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component1Base, i)) = value;
Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component2Base, i)) = value; Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component2Base, i)) = value;
@ -213,12 +211,10 @@ internal abstract partial class HeifColorConverterBase
if (Vector256.IsHardwareAccelerated && i <= length - Vector256<float>.Count) if (Vector256.IsHardwareAccelerated && i <= length - Vector256<float>.Count)
{ {
Vector256<float> bias = Vector256.Create(parameters.LumaBias);
Vector256<float> scale = Vector256.Create(parameters.LumaScale);
int oneVectorFromEnd = length - Vector256<float>.Count; int oneVectorFromEnd = length - Vector256<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<float>.Count) for (; i <= oneVectorFromEnd; i += Vector256<float>.Count)
{ {
Vector256<float> value = (Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component0Base, i)) - bias) / scale; Vector256<float> value = Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component0Base, i));
Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component0Base, i)) = value; Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component0Base, i)) = value;
Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component1Base, i)) = value; Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component1Base, i)) = value;
Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component2Base, i)) = value; Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component2Base, i)) = value;
@ -227,12 +223,10 @@ internal abstract partial class HeifColorConverterBase
if (Vector128.IsHardwareAccelerated && i <= length - Vector128<float>.Count) if (Vector128.IsHardwareAccelerated && i <= length - Vector128<float>.Count)
{ {
Vector128<float> bias = Vector128.Create(parameters.LumaBias);
Vector128<float> scale = Vector128.Create(parameters.LumaScale);
int oneVectorFromEnd = length - Vector128<float>.Count; int oneVectorFromEnd = length - Vector128<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count) for (; i <= oneVectorFromEnd; i += Vector128<float>.Count)
{ {
Vector128<float> value = (Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component0Base, i)) - bias) / scale; Vector128<float> value = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component0Base, i));
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component0Base, i)) = value; Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component0Base, i)) = value;
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component1Base, i)) = value; Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component1Base, i)) = value;
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component2Base, i)) = value; Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component2Base, i)) = value;
@ -241,7 +235,7 @@ internal abstract partial class HeifColorConverterBase
for (; i < length; i++) for (; i < length; i++)
{ {
float value = (Unsafe.Add(ref component0Base, i) - parameters.LumaBias) / parameters.LumaScale; float value = Unsafe.Add(ref component0Base, i);
Unsafe.Add(ref component0Base, i) = value; Unsafe.Add(ref component0Base, i) = value;
Unsafe.Add(ref component1Base, i) = value; Unsafe.Add(ref component1Base, i) = value;
Unsafe.Add(ref component2Base, i) = value; Unsafe.Add(ref component2Base, i) = value;
@ -250,26 +244,16 @@ internal abstract partial class HeifColorConverterBase
return; return;
} }
float chromaBias = this.ChromaBias; // Samples are normalized before chroma interpolation. This traversal applies only the color
float chromaScale = this.ChromaScale; // model, preserving those fractional values through the descending SIMD widths.
// Descending widths preserve vector execution for the remainder left by a wider register. Divide by the
// signaled ranges directly because multiplying by rounded reciprocals changes exact output-code boundaries.
if (Vector512.IsHardwareAccelerated && i <= length - Vector512<float>.Count) if (Vector512.IsHardwareAccelerated && i <= length - Vector512<float>.Count)
{ {
Vector512<float> lumaBias = Vector512.Create(parameters.LumaBias);
Vector512<float> lumaScale = Vector512.Create(parameters.LumaScale);
Vector512<float> chromaBiasVector = Vector512.Create(chromaBias);
Vector512<float> chromaScaleVector = Vector512.Create(chromaScale);
int oneVectorFromEnd = length - Vector512<float>.Count; int oneVectorFromEnd = length - Vector512<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<float>.Count) for (; i <= oneVectorFromEnd; i += Vector512<float>.Count)
{ {
ref Vector512<float> c0 = ref Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component0Base, i)); ref Vector512<float> c0 = ref Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component0Base, i));
ref Vector512<float> c1 = ref Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component1Base, i)); ref Vector512<float> c1 = ref Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component1Base, i));
ref Vector512<float> c2 = ref Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component2Base, i)); ref Vector512<float> c2 = ref Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component2Base, i));
c0 = (c0 - lumaBias) / lumaScale;
c1 = (c1 - chromaBiasVector) / chromaScaleVector;
c2 = (c2 - chromaBiasVector) / chromaScaleVector;
TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters); TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters);
} }
@ -277,19 +261,12 @@ internal abstract partial class HeifColorConverterBase
if (Vector256.IsHardwareAccelerated && i <= length - Vector256<float>.Count) if (Vector256.IsHardwareAccelerated && i <= length - Vector256<float>.Count)
{ {
Vector256<float> lumaBias = Vector256.Create(parameters.LumaBias);
Vector256<float> lumaScale = Vector256.Create(parameters.LumaScale);
Vector256<float> chromaBiasVector = Vector256.Create(chromaBias);
Vector256<float> chromaScaleVector = Vector256.Create(chromaScale);
int oneVectorFromEnd = length - Vector256<float>.Count; int oneVectorFromEnd = length - Vector256<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<float>.Count) for (; i <= oneVectorFromEnd; i += Vector256<float>.Count)
{ {
ref Vector256<float> c0 = ref Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component0Base, i)); ref Vector256<float> c0 = ref Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component0Base, i));
ref Vector256<float> c1 = ref Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component1Base, i)); ref Vector256<float> c1 = ref Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component1Base, i));
ref Vector256<float> c2 = ref Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component2Base, i)); ref Vector256<float> c2 = ref Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component2Base, i));
c0 = (c0 - lumaBias) / lumaScale;
c1 = (c1 - chromaBiasVector) / chromaScaleVector;
c2 = (c2 - chromaBiasVector) / chromaScaleVector;
TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters); TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters);
} }
@ -297,19 +274,12 @@ internal abstract partial class HeifColorConverterBase
if (Vector128.IsHardwareAccelerated && i <= length - Vector128<float>.Count) if (Vector128.IsHardwareAccelerated && i <= length - Vector128<float>.Count)
{ {
Vector128<float> lumaBias = Vector128.Create(parameters.LumaBias);
Vector128<float> lumaScale = Vector128.Create(parameters.LumaScale);
Vector128<float> chromaBiasVector = Vector128.Create(chromaBias);
Vector128<float> chromaScaleVector = Vector128.Create(chromaScale);
int oneVectorFromEnd = length - Vector128<float>.Count; int oneVectorFromEnd = length - Vector128<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count) for (; i <= oneVectorFromEnd; i += Vector128<float>.Count)
{ {
ref Vector128<float> c0 = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component0Base, i)); ref Vector128<float> c0 = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component0Base, i));
ref Vector128<float> c1 = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component1Base, i)); ref Vector128<float> c1 = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component1Base, i));
ref Vector128<float> c2 = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component2Base, i)); ref Vector128<float> c2 = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component2Base, i));
c0 = (c0 - lumaBias) / lumaScale;
c1 = (c1 - chromaBiasVector) / chromaScaleVector;
c2 = (c2 - chromaBiasVector) / chromaScaleVector;
TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters); TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters);
} }
@ -318,9 +288,9 @@ internal abstract partial class HeifColorConverterBase
// Scalar conversion is reserved for the zero-to-three samples left after the SIMD cascade. // Scalar conversion is reserved for the zero-to-three samples left after the SIMD cascade.
for (; i < length; i++) for (; i < length; i++)
{ {
float c0 = (Unsafe.Add(ref component0Base, i) - parameters.LumaBias) / parameters.LumaScale; float c0 = Unsafe.Add(ref component0Base, i);
float c1 = (Unsafe.Add(ref component1Base, i) - chromaBias) / chromaScale; float c1 = Unsafe.Add(ref component1Base, i);
float c2 = (Unsafe.Add(ref component2Base, i) - chromaBias) / chromaScale; float c2 = Unsafe.Add(ref component2Base, i);
TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters); TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters);
Unsafe.Add(ref component0Base, i) = c0; Unsafe.Add(ref component0Base, i) = c0;
Unsafe.Add(ref component1Base, i) = c1; Unsafe.Add(ref component1Base, i) = c1;
@ -338,7 +308,7 @@ internal abstract partial class HeifColorConverterBase
HeifColorConversionParameters parameters = this.Parameters; HeifColorConversionParameters parameters = this.Parameters;
// The unpacker supplies three planar RGB rows. These same buffers become the destination component // The unpacker supplies three planar RGB rows. These same buffers become the destination component
// rows after each operator call, so encoding retains JPEG's planar contract without another allocation. // rows after each operator call, so encoding retains the planar contract without another allocation.
ref float component0Base = ref MemoryMarshal.GetReference(component0); ref float component0Base = ref MemoryMarshal.GetReference(component0);
ref float component1Base = ref MemoryMarshal.GetReference(component1); ref float component1Base = ref MemoryMarshal.GetReference(component1);
ref float component2Base = ref MemoryMarshal.GetReference(component2); ref float component2Base = ref MemoryMarshal.GetReference(component2);
@ -346,7 +316,7 @@ internal abstract partial class HeifColorConverterBase
int i = 0; int i = 0;
// RGB normalization is part of the vector load, and each operator returns planar components through // RGB normalization is part of the vector load, and each operator returns planar components through
// out parameters. This is the same input/output shape used by JPEG's encoder-side color operators. // out parameters. The input and output planes share the same row coordinates.
if (Vector512.IsHardwareAccelerated) if (Vector512.IsHardwareAccelerated)
{ {
int oneVectorFromEnd = length - Vector512<float>.Count; int oneVectorFromEnd = length - Vector512<float>.Count;

443
src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifPlanarColorConverter.cs

@ -2,8 +2,14 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers; using System.Buffers;
using System.Numerics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.ColorProfiles;
using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.Formats.Heif.Components.Alpha;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Heif.Components; namespace SixLabors.ImageSharp.Formats.Heif.Components;
@ -33,19 +39,6 @@ internal static class HeifPlanarColorConverter
/// </summary> /// </summary>
private const float UShortMaximum = ushort.MaxValue; private const float UShortMaximum = ushort.MaxValue;
/// <summary>
/// Converts complete native unsigned 16-bit component planes to packed pixels.
/// </summary>
public static void ConvertToRgb<TPixel, TBuffer>(
Configuration configuration,
TBuffer buffer,
ImageFrame<TPixel> image,
in HeifColorConversionParameters parameters,
HeifColorConversionMode mode)
where TPixel : unmanaged, IPixel<TPixel>
where TBuffer : struct, IHeifPlanarSampleBuffer<ushort>
=> ConvertToRgb(configuration, buffer, image, in parameters, mode, 0, 0);
/// <summary> /// <summary>
/// Converts a region of native unsigned 16-bit component storage to packed pixels and selects eligible exact integer kernels. /// Converts a region of native unsigned 16-bit component storage to packed pixels and selects eligible exact integer kernels.
/// </summary> /// </summary>
@ -53,23 +46,35 @@ internal static class HeifPlanarColorConverter
/// <typeparam name="TBuffer">The codec adapter exposing the native component planes.</typeparam> /// <typeparam name="TBuffer">The codec adapter exposing the native component planes.</typeparam>
/// <param name="configuration">The configuration used for allocation and pixel conversion.</param> /// <param name="configuration">The configuration used for allocation and pixel conversion.</param>
/// <param name="buffer">The native component-plane buffer.</param> /// <param name="buffer">The native component-plane buffer.</param>
/// <param name="image">The destination image frame.</param> /// <param name="destination">The exact destination pixel region.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param> /// <param name="parameters">The resolved H.273 conversion parameters.</param>
/// <param name="mode">The resolved H.273 conversion mode.</param> /// <param name="mode">The resolved H.273 conversion mode.</param>
/// <param name="sourceX">The horizontal luma-sample offset of the first converted pixel.</param> /// <param name="sourceX">The horizontal luma-sample offset of the first converted pixel.</param>
/// <param name="sourceY">The vertical luma-sample offset of the first converted pixel.</param> /// <param name="sourceY">The vertical luma-sample offset of the first converted pixel.</param>
/// <param name="sourceSize">The extent of the source region before rotation and mirroring.</param>
/// <param name="transform">The rotation and mirroring applied within the destination region.</param>
/// <param name="profile">The source profile selected for conversion, or null to preserve source colors.</param>
/// <param name="alpha">The matching normalized auxiliary rows, or null for opaque pixels.</param>
/// <param name="premultiplied">Whether source RGB is associated with alpha.</param>
/// <param name="chromaUpsampling">The chroma reconstruction mode.</param>
public static void ConvertToRgb<TPixel, TBuffer>( public static void ConvertToRgb<TPixel, TBuffer>(
Configuration configuration, Configuration configuration,
TBuffer buffer, TBuffer buffer,
ImageFrame<TPixel> image, Buffer2DRegion<TPixel> destination,
in HeifColorConversionParameters parameters, in HeifColorConversionParameters parameters,
HeifColorConversionMode mode, HeifColorConversionMode mode,
int sourceX, int sourceX,
int sourceY) int sourceY,
Size sourceSize,
HeifPixelTransform transform,
IccProfile? profile,
HeifAlphaRowSource? alpha,
bool premultiplied,
HeifChromaUpsampling chromaUpsampling)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
where TBuffer : struct, IHeifPlanarSampleBuffer<ushort> where TBuffer : struct, IHeifPlanarSampleBuffer<ushort>
{ {
if (HeifYuvToRgb8Converter.SupportsFixedPointConversion( if (chromaUpsampling != HeifChromaUpsampling.Bilinear && profile is null && alpha is null && HeifYuvToRgb8Converter.SupportsFixedPointConversion(
buffer.ChromaSubsamplingX, buffer.ChromaSubsamplingX,
buffer.ChromaSubsamplingY, buffer.ChromaSubsamplingY,
buffer.LumaBitDepth, buffer.LumaBitDepth,
@ -80,35 +85,26 @@ internal static class HeifPlanarColorConverter
{ {
// The fixed-point operator preserves the exact code-value rounding used by the verified eight-bit // The fixed-point operator preserves the exact code-value rounding used by the verified eight-bit
// presentation path. Selection belongs here so no codec can acquire a private color-conversion route. // presentation path. Selection belongs here so no codec can acquire a private color-conversion route.
HeifYuvToRgb8Converter.ConvertFixedPoint(configuration, buffer, image, in parameters, sourceX, sourceY); HeifYuvToRgb8Converter.ConvertFixedPoint(configuration, buffer, destination, in parameters, sourceX, sourceY, sourceSize, transform);
return; return;
} }
ConvertToRgb<TPixel, TBuffer, ushort, HeifUShortSampleConverter>( ConvertToRgb<TPixel, TBuffer, ushort, HeifUShortSampleConverter>(
configuration, configuration,
buffer, buffer,
image, destination,
in parameters, in parameters,
mode, mode,
sourceX, sourceX,
sourceY); sourceY,
sourceSize,
transform,
profile,
alpha,
premultiplied,
chromaUpsampling);
} }
/// <summary>
/// Converts complete native component planes to packed pixels.
/// </summary>
public static void ConvertToRgb<TPixel, TBuffer, TSample, TLoader>(
Configuration configuration,
TBuffer buffer,
ImageFrame<TPixel> image,
in HeifColorConversionParameters parameters,
HeifColorConversionMode mode)
where TPixel : unmanaged, IPixel<TPixel>
where TBuffer : struct, IHeifPlanarSampleBuffer<TSample>
where TSample : unmanaged
where TLoader : struct, IHeifSampleConverter<TSample>
=> ConvertToRgb<TPixel, TBuffer, TSample, TLoader>(configuration, buffer, image, in parameters, mode, 0, 0);
/// <summary> /// <summary>
/// Converts a region of native component planes to packed pixels. /// Converts a region of native component planes to packed pixels.
/// </summary> /// </summary>
@ -118,38 +114,71 @@ internal static class HeifPlanarColorConverter
/// <typeparam name="TLoader">The SIMD widening operations for the sample type.</typeparam> /// <typeparam name="TLoader">The SIMD widening operations for the sample type.</typeparam>
/// <param name="configuration">The configuration used for allocation and pixel conversion.</param> /// <param name="configuration">The configuration used for allocation and pixel conversion.</param>
/// <param name="buffer">The native component-plane buffer.</param> /// <param name="buffer">The native component-plane buffer.</param>
/// <param name="image">The destination image frame.</param> /// <param name="destination">The exact destination pixel region.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param> /// <param name="parameters">The resolved H.273 conversion parameters.</param>
/// <param name="mode">The resolved H.273 conversion mode.</param> /// <param name="mode">The resolved H.273 conversion mode.</param>
/// <param name="sourceX">The horizontal luma-sample offset of the first converted pixel.</param> /// <param name="sourceX">The horizontal luma-sample offset of the first converted pixel.</param>
/// <param name="sourceY">The vertical luma-sample offset of the first converted pixel.</param> /// <param name="sourceY">The vertical luma-sample offset of the first converted pixel.</param>
/// <param name="sourceSize">The extent of the source region before rotation and mirroring.</param>
/// <param name="transform">The rotation and mirroring applied within the destination region.</param>
/// <param name="profile">The source profile selected for conversion, or null to preserve source colors.</param>
/// <param name="alpha">The matching normalized auxiliary rows, or null for opaque pixels.</param>
/// <param name="premultiplied">Whether source RGB is associated with alpha.</param>
/// <param name="chromaUpsampling">The chroma reconstruction mode.</param>
public static void ConvertToRgb<TPixel, TBuffer, TSample, TLoader>( public static void ConvertToRgb<TPixel, TBuffer, TSample, TLoader>(
Configuration configuration, Configuration configuration,
TBuffer buffer, TBuffer buffer,
ImageFrame<TPixel> image, Buffer2DRegion<TPixel> destination,
in HeifColorConversionParameters parameters, in HeifColorConversionParameters parameters,
HeifColorConversionMode mode, HeifColorConversionMode mode,
int sourceX, int sourceX,
int sourceY) int sourceY,
Size sourceSize,
HeifPixelTransform transform,
IccProfile? profile,
HeifAlphaRowSource? alpha,
bool premultiplied,
HeifChromaUpsampling chromaUpsampling)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
where TBuffer : struct, IHeifPlanarSampleBuffer<TSample> where TBuffer : struct, IHeifPlanarSampleBuffer<TSample>
where TSample : unmanaged where TSample : unmanaged
where TLoader : struct, IHeifSampleConverter<TSample> where TLoader : struct, IHeifSampleConverter<TSample>
{ {
HeifColorConverterBase colorConverter = HeifColorConverterBase.Create(mode, in parameters, buffer.IsMonochrome); HeifColorConverterBase colorConverter = HeifColorConverterBase.Create(mode, in parameters, buffer.IsMonochrome);
ColorProfileConverter? profileConverter = profile is null
? null
: new ColorProfileConverter(new ColorConversionOptions
{
MemoryAllocator = configuration.MemoryAllocator,
SourceIccProfile = profile,
TargetIccProfile = CompactSrgbV4Profile.Profile,
});
YuvToRgbRowConverter<TPixel, TBuffer, TSample, TLoader> converter = new( YuvToRgbRowConverter<TPixel, TBuffer, TSample, TLoader> converter = new(
configuration, configuration,
buffer, buffer,
image, sourceSize.Width,
colorConverter, colorConverter,
profileConverter,
alpha,
premultiplied,
sourceX, sourceX,
sourceY); sourceY,
chromaUpsampling);
using IMemoryOwner<float> scratchOwner = configuration.MemoryAllocator.Allocate<float>(converter.BufferLength); using IMemoryOwner<float> scratchOwner = configuration.MemoryAllocator.Allocate<float>(converter.BufferLength);
Span<float> scratch = scratchOwner.GetSpan(); Span<float> scratch = scratchOwner.GetSpan();
for (int y = 0; y < image.Height; y++) Matrix3x2 matrix = transform.GetMatrix(sourceSize);
Point origin = HeifPixelTransform.Transform(0, 0, matrix);
Size columnStep = new((int)matrix.M11, (int)matrix.M12);
Size rowStep = new((int)matrix.M21, (int)matrix.M22);
// The decoder's region bounds already include crop and tile placement. Resolve orientation once;
// each row advances by an integer basis vector and packing writes directly into that region.
for (int y = 0; y < sourceSize.Height; y++)
{ {
converter.Convert(y, scratch); converter.Convert(y, scratch, destination, origin, columnStep);
origin += rowStep;
} }
} }
@ -343,15 +372,23 @@ internal static class HeifPlanarColorConverter
private TBuffer buffer; private TBuffer buffer;
/// <summary> /// <summary>
/// The destination image frame. /// The exact destination pixel region.
/// </summary> /// </summary>
private readonly ImageFrame<TPixel> image; private readonly int width;
/// <summary> /// <summary>
/// The selected H.273 color converter. /// The selected H.273 color converter.
/// </summary> /// </summary>
private readonly HeifColorConverterBase colorConverter; private readonly HeifColorConverterBase colorConverter;
/// <summary>
/// The profile transform reused by every component row in the region.
/// </summary>
private readonly ColorProfileConverter? profileConverter;
private readonly HeifAlphaRowSource? alpha;
private readonly bool premultiplied;
/// <summary> /// <summary>
/// The horizontal luma-sample offset of the output window. /// The horizontal luma-sample offset of the output window.
/// </summary> /// </summary>
@ -403,7 +440,7 @@ internal static class HeifPlanarColorConverter
private readonly bool isMonochrome; private readonly bool isMonochrome;
/// <summary> /// <summary>
/// Whether the destination uses JPEG-compatible eight-bit RGB plane packing. /// Whether the destination uses eight-bit RGB plane packing.
/// </summary> /// </summary>
private readonly bool usesBytePacking; private readonly bool usesBytePacking;
@ -412,27 +449,43 @@ internal static class HeifPlanarColorConverter
/// </summary> /// </summary>
private readonly bool reconstructCompleteRow; private readonly bool reconstructCompleteRow;
/// <summary>
/// Whether subsampled chroma is reconstructed with bilinear interpolation.
/// </summary>
private readonly bool useBilinear;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="YuvToRgbRowConverter{TPixel, TBuffer, TSample, TLoader}"/> struct. /// Initializes a new instance of the <see cref="YuvToRgbRowConverter{TPixel, TBuffer, TSample, TLoader}"/> struct.
/// </summary> /// </summary>
/// <param name="configuration">The configuration used for pixel conversion.</param> /// <param name="configuration">The configuration used for pixel conversion.</param>
/// <param name="buffer">The codec adapter exposing the reconstructed component planes.</param> /// <param name="buffer">The codec adapter exposing the reconstructed component planes.</param>
/// <param name="image">The destination image frame.</param> /// <param name="width">The source row width.</param>
/// <param name="colorConverter">The selected H.273 color converter.</param> /// <param name="colorConverter">The selected H.273 color converter.</param>
/// <param name="profileConverter">The profile transform for this region, or null to preserve source colors.</param>
/// <param name="alpha">The auxiliary rows, or null for opaque pixels.</param>
/// <param name="premultiplied">Whether source RGB is associated with alpha.</param>
/// <param name="sourceX">The horizontal luma-sample offset of the output window.</param> /// <param name="sourceX">The horizontal luma-sample offset of the output window.</param>
/// <param name="sourceY">The vertical luma-sample offset of the output window.</param> /// <param name="sourceY">The vertical luma-sample offset of the output window.</param>
/// <param name="chromaUpsampling">The chroma reconstruction mode.</param>
public YuvToRgbRowConverter( public YuvToRgbRowConverter(
Configuration configuration, Configuration configuration,
TBuffer buffer, TBuffer buffer,
ImageFrame<TPixel> image, int width,
HeifColorConverterBase colorConverter, HeifColorConverterBase colorConverter,
ColorProfileConverter? profileConverter,
HeifAlphaRowSource? alpha,
bool premultiplied,
int sourceX, int sourceX,
int sourceY) int sourceY,
HeifChromaUpsampling chromaUpsampling)
{ {
this.configuration = configuration; this.configuration = configuration;
this.buffer = buffer; this.buffer = buffer;
this.image = image; this.width = width;
this.colorConverter = colorConverter; this.colorConverter = colorConverter;
this.profileConverter = profileConverter;
this.alpha = alpha;
this.premultiplied = premultiplied;
this.sourceX = sourceX; this.sourceX = sourceX;
this.sourceY = sourceY; this.sourceY = sourceY;
this.bufferWidth = buffer.Width; this.bufferWidth = buffer.Width;
@ -444,11 +497,13 @@ internal static class HeifPlanarColorConverter
this.usesBytePacking = buffer.LumaBitDepth == 8 && (buffer.IsMonochrome || buffer.ChromaBitDepth == 8); this.usesBytePacking = buffer.LumaBitDepth == 8 && (buffer.IsMonochrome || buffer.ChromaBitDepth == 8);
this.chromaWidth = (buffer.Width + (1 << this.subsamplingX) - 1) >> this.subsamplingX; this.chromaWidth = (buffer.Width + (1 << this.subsamplingX) - 1) >> this.subsamplingX;
this.chromaHeight = (buffer.Height + (1 << this.subsamplingY) - 1) >> this.subsamplingY; this.chromaHeight = (buffer.Height + (1 << this.subsamplingY) - 1) >> this.subsamplingY;
this.reconstructCompleteRow = sourceX != 0 || image.Width != buffer.Width; this.reconstructCompleteRow = sourceX != 0 || width != buffer.Width;
this.useBilinear = chromaUpsampling == HeifChromaUpsampling.Bilinear
|| (chromaUpsampling == HeifChromaUpsampling.Auto && buffer.ChromaBitDepth > 8);
} }
/// <summary> /// <summary>
/// Gets a value indicating whether the destination uses JPEG-compatible eight-bit RGB plane packing. /// Gets a value indicating whether the destination uses eight-bit RGB plane packing.
/// </summary> /// </summary>
public readonly bool UsesBytePacking => this.usesBytePacking; public readonly bool UsesBytePacking => this.usesBytePacking;
@ -459,16 +514,18 @@ internal static class HeifPlanarColorConverter
{ {
get get
{ {
int componentLength = this.image.Width * 3; int componentLength = this.width * 3;
if (!this.isMonochrome && this.subsamplingX != 0) if (this.useBilinear && !this.isMonochrome && this.subsamplingX != 0)
{ {
// Full-image conversion reconstructs directly into the component rows. Cropped conversion // Full-destination conversion reconstructs directly into the component rows. Cropped conversion
// retains the complete coded row so interpolation phase is preserved at the window boundary. // retains the complete coded row so interpolation phase is preserved at the window boundary.
componentLength += (this.chromaWidth * 2) + (this.reconstructCompleteRow ? this.bufferWidth : 0); componentLength += (this.chromaWidth * 2) + (this.reconstructCompleteRow ? this.bufferWidth : 0);
} }
int packedRowCount = this.UsesBytePacking ? 1 : 2; // ICC interleaving and final pixel packing occur sequentially, so the same scratch
return componentLength + (this.image.Width * packedRowCount); // storage holds both. No additional buffer or allocation is needed per row.
int packedRowCount = this.profileConverter is not null ? 4 : this.UsesBytePacking && this.alpha is null ? 1 : 2;
return componentLength + (this.width * packedRowCount);
} }
} }
@ -477,88 +534,193 @@ internal static class HeifPlanarColorConverter
/// </summary> /// </summary>
/// <param name="y">The zero-based output row.</param> /// <param name="y">The zero-based output row.</param>
/// <param name="scratch">The reusable pooled row buffer.</param> /// <param name="scratch">The reusable pooled row buffer.</param>
public void Convert(int y, Span<float> scratch) /// <param name="destination">The exact output region receiving the converted pixels.</param>
/// <param name="origin">The final location of the first pixel in this source row.</param>
/// <param name="step">The destination increment for each source pixel.</param>
public void Convert(int y, Span<float> scratch, Buffer2DRegion<TPixel> destination, Point origin, Size step)
{ {
int width = this.image.Width; int width = this.width;
Span<float> red = scratch[..width]; Span<float> red = scratch[..width];
Span<float> green = scratch.Slice(width, width); Span<float> green = scratch.Slice(width, width);
Span<float> blue = scratch.Slice(width * 2, width); Span<float> blue = scratch.Slice(width * 2, width);
int sourceY = y + this.sourceY; int sourceY = y + this.sourceY;
ReadOnlySpan<TSample> luma = this.buffer.GetLumaRowSpan(sourceY).Slice(this.sourceX, width); ReadOnlySpan<TSample> luma = this.buffer.GetLumaRowSpan(sourceY).Slice(this.sourceX, width);
HeifSampleConversion.ConvertSamplesToFloat<TSample, TLoader>(luma, red); HeifSampleConversion.ConvertSamplesToFloat<TSample, TLoader>(luma, red, this.colorConverter.LumaBias, this.colorConverter.LumaScale);
int packedOffset = width * 3; int packedOffset = width * 3;
if (!this.isMonochrome) if (!this.isMonochrome)
{ {
GetChromaCoordinates( if (this.useBilinear)
sourceY,
this.subsamplingY,
this.chromaPositionY,
this.chromaHeight - 1,
out int y0,
out int y1,
out int y1Weight);
ReadOnlySpan<TSample> cb0 = this.buffer.GetChromaBlueRowSpan(y0);
ReadOnlySpan<TSample> cb1 = this.buffer.GetChromaBlueRowSpan(y1);
ReadOnlySpan<TSample> cr0 = this.buffer.GetChromaRedRowSpan(y0);
ReadOnlySpan<TSample> cr1 = this.buffer.GetChromaRedRowSpan(y1);
if (this.subsamplingX == 0)
{ {
HeifSampleConversion.ConvertSamplesToFloat<TSample, TLoader>(cb0.Slice(this.sourceX, width), green); GetChromaCoordinates(
HeifSampleConversion.ConvertSamplesToFloat<TSample, TLoader>(cr0.Slice(this.sourceX, width), blue); sourceY,
this.subsamplingY,
this.chromaPositionY,
this.chromaHeight - 1,
out int y0,
out int y1,
out int y1Weight);
ReadOnlySpan<TSample> cb0 = this.buffer.GetChromaBlueRowSpan(y0);
ReadOnlySpan<TSample> cb1 = this.buffer.GetChromaBlueRowSpan(y1);
ReadOnlySpan<TSample> cr0 = this.buffer.GetChromaRedRowSpan(y0);
ReadOnlySpan<TSample> cr1 = this.buffer.GetChromaRedRowSpan(y1);
if (this.subsamplingX == 0)
{
HeifSampleConversion.ConvertSamplesToFloat<TSample, TLoader>(
cb0.Slice(this.sourceX, width), green, this.colorConverter.ChromaBias, this.colorConverter.ChromaScale);
HeifSampleConversion.ConvertSamplesToFloat<TSample, TLoader>(
cr0.Slice(this.sourceX, width), blue, this.colorConverter.ChromaBias, this.colorConverter.ChromaScale);
}
else
{
Span<float> chroma0 = scratch.Slice(packedOffset, this.chromaWidth);
Span<float> chroma1 = scratch.Slice(packedOffset + this.chromaWidth, this.chromaWidth);
Span<float> reconstructed = this.reconstructCompleteRow
? scratch.Slice(packedOffset + (this.chromaWidth * 2), this.bufferWidth)
: green;
bool isCenteredX = this.chromaPositionX == 1;
HeifSampleConversion.ReconstructChromaRowBilinear<TSample, TLoader>(
cb0,
cb1,
y1Weight,
this.subsamplingX,
isCenteredX,
reconstructed,
chroma0,
chroma1,
this.colorConverter.ChromaBias,
this.colorConverter.ChromaScale);
if (this.reconstructCompleteRow)
{
reconstructed.Slice(this.sourceX, width).CopyTo(green);
}
reconstructed = this.reconstructCompleteRow ? reconstructed : blue;
HeifSampleConversion.ReconstructChromaRowBilinear<TSample, TLoader>(
cr0,
cr1,
y1Weight,
this.subsamplingX,
isCenteredX,
reconstructed,
chroma0,
chroma1,
this.colorConverter.ChromaBias,
this.colorConverter.ChromaScale);
if (this.reconstructCompleteRow)
{
reconstructed.Slice(this.sourceX, width).CopyTo(blue);
}
packedOffset += (this.chromaWidth * 2) + (this.reconstructCompleteRow ? this.bufferWidth : 0);
}
} }
else else
{ {
Span<float> chroma0 = scratch.Slice(packedOffset, this.chromaWidth); // Each chroma row serves two luma rows when vertically subsampled. Use absolute
Span<float> chroma1 = scratch.Slice(packedOffset + this.chromaWidth, this.chromaWidth); // source coordinates so cropped regions retain the coded sample-replication phase.
Span<float> reconstructed = this.reconstructCompleteRow int chromaY = sourceY >> this.subsamplingY;
? scratch.Slice(packedOffset + (this.chromaWidth * 2), this.bufferWidth) ReadOnlySpan<TSample> cb = this.buffer.GetChromaBlueRowSpan(chromaY);
: green; ReadOnlySpan<TSample> cr = this.buffer.GetChromaRedRowSpan(chromaY);
if (this.subsamplingX == 0)
bool isCenteredX = this.chromaPositionX == 1;
HeifSampleConversion.ReconstructChromaRow<TSample, TLoader>(
cb0,
cb1,
y1Weight,
this.subsamplingX,
isCenteredX,
reconstructed,
chroma0,
chroma1);
if (this.reconstructCompleteRow)
{ {
reconstructed.Slice(this.sourceX, width).CopyTo(green); HeifSampleConversion.ConvertSamplesToFloat<TSample, TLoader>(
cb.Slice(this.sourceX, width), green, this.colorConverter.ChromaBias, this.colorConverter.ChromaScale);
HeifSampleConversion.ConvertSamplesToFloat<TSample, TLoader>(
cr.Slice(this.sourceX, width), blue, this.colorConverter.ChromaBias, this.colorConverter.ChromaScale);
} }
else
reconstructed = this.reconstructCompleteRow ? reconstructed : blue;
HeifSampleConversion.ReconstructChromaRow<TSample, TLoader>(
cr0,
cr1,
y1Weight,
this.subsamplingX,
isCenteredX,
reconstructed,
chroma0,
chroma1);
if (this.reconstructCompleteRow)
{ {
reconstructed.Slice(this.sourceX, width).CopyTo(blue); HeifSampleConversion.ReconstructChromaRow<TSample, TLoader>(
cb, this.sourceX, green, this.colorConverter.ChromaBias, this.colorConverter.ChromaScale);
HeifSampleConversion.ReconstructChromaRow<TSample, TLoader>(
cr, this.sourceX, blue, this.colorConverter.ChromaBias, this.colorConverter.ChromaScale);
} }
packedOffset += (this.chromaWidth * 2) + (this.reconstructCompleteRow ? this.bufferWidth : 0);
} }
} }
this.colorConverter.ConvertToRgbInPlace(red, green, blue); this.colorConverter.ConvertToRgbInPlace(red, green, blue);
Span<TPixel> destination = this.image.PixelBuffer.DangerousGetRowSpan(y); ReadOnlySpan<float> alpha = this.alpha is null ? default : this.alpha.ReadRow(y);
if (this.premultiplied)
{
HeifColorConverterBase.UnassociateRgb(red, green, blue, alpha);
}
Span<float> packedStorage = scratch[packedOffset..]; Span<float> packedStorage = scratch[packedOffset..];
if (this.UsesBytePacking) if (this.profileConverter is not null)
{
this.colorConverter.ConvertRgbToSrgbInPlace(
red, green, blue, this.profileConverter, MemoryMarshal.Cast<float, Rgb>(packedStorage)[..width]);
// Keep the converted RGB and auxiliary alpha in floating point until TPixel performs its
// final conversion. An integer intermediate would discard precision for floating-point pixels.
// ICC's interleaved RGB is no longer live, so its scratch storage now holds RGBA vectors.
Span<Vector4> vectors = MemoryMarshal.Cast<float, Vector4>(packedStorage)[..width];
int x = 0;
if (Vector128.IsHardwareAccelerated)
{
// Load four samples per plane and transpose them into four RGBA pixels. Opaque alpha
// supplies four ones; auxiliary alpha has already been normalized by its row source.
for (; x <= width - Vector128<float>.Count; x += Vector128<float>.Count)
{
Vector128<float> r = Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(red), (nuint)x);
Vector128<float> g = Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(green), (nuint)x);
Vector128<float> b = Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(blue), (nuint)x);
Vector128<float> a = this.alpha is null
? Vector128.Create(1F)
: Vector128.LoadUnsafe(ref MemoryMarshal.GetReference(alpha), (nuint)x);
HeifColorConverterBase.Transpose4(
r,
g,
b,
a,
out Vector128<float> p0,
out Vector128<float> p1,
out Vector128<float> p2,
out Vector128<float> p3);
vectors[x] = p0.AsVector4();
vectors[x + 1] = p1.AsVector4();
vectors[x + 2] = p2.AsVector4();
vectors[x + 3] = p3.AsVector4();
}
}
for (; x < width; x++)
{
vectors[x] = new Vector4(red[x], green[x], blue[x], this.alpha is null ? 1F : alpha[x]);
}
if (step.Width == 1)
{
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
vectors,
destination.DangerousGetRowSpan(origin.Y).Slice(origin.X, width),
PixelConversionModifiers.Scale);
}
else
{
// Reversed rows and rotated columns write directly into their final locations.
for (x = 0; x < width; x++)
{
destination.DangerousGetRowSpan(origin.Y)[origin.X] = TPixel.FromUnassociatedScaledVector4(vectors[x]);
origin += step;
}
}
return;
}
if (this.UsesBytePacking && this.alpha is null)
{ {
// This is JPEG's planar packing contract. Existing pixel-specific SIMD packers therefore own the // The shared planar packing contract lets existing pixel-specific SIMD packers own the
// final RGB-to-TPixel conversion rather than a HEIF-codec-specific per-pixel implementation. // final RGB-to-TPixel conversion rather than a HEIF-codec-specific per-pixel implementation.
Span<byte> byteStorage = MemoryMarshal.AsBytes(packedStorage)[..(width * 3)]; Span<byte> byteStorage = MemoryMarshal.AsBytes(packedStorage)[..(width * 3)];
Span<byte> redBytes = byteStorage[..width]; Span<byte> redBytes = byteStorage[..width];
@ -568,18 +730,59 @@ internal static class HeifPlanarColorConverter
SimdUtils.NormalizedFloatToByteSaturate(green, greenBytes); SimdUtils.NormalizedFloatToByteSaturate(green, greenBytes);
SimdUtils.NormalizedFloatToByteSaturate(blue, blueBytes); SimdUtils.NormalizedFloatToByteSaturate(blue, blueBytes);
PixelOperations<TPixel>.Instance.PackFromRgbPlanes(redBytes, greenBytes, blueBytes, destination); if (step.Width == 1)
{
PixelOperations<TPixel>.Instance.PackFromRgbPlanes(
redBytes, greenBytes, blueBytes, destination.DangerousGetRowSpan(origin.Y).Slice(origin.X, width));
}
else
{
// A reversed row or column is not a contiguous span. Construct the final pixel at its
// destination instead of packing a temporary TPixel row and reading it back to scatter.
for (int x = 0; x < width; x++)
{
destination.DangerousGetRowSpan(origin.Y)[origin.X] = TPixel.FromRgb24(new Rgb24(redBytes[x], greenBytes[x], blueBytes[x]));
origin += step;
}
}
return; return;
} }
Span<Rgba64> packed = MemoryMarshal.Cast<float, Rgba64>(packedStorage)[..width]; Span<Rgba64> packed = MemoryMarshal.Cast<float, Rgba64>(packedStorage)[..width];
HeifSampleConversion.PackRgba64(red, green, blue, packed); HeifSampleConversion.PackRgba64(red, green, blue, packed);
PixelOperations<TPixel>.Instance.FromRgba64(this.configuration, packed, destination); if (this.alpha is not null)
{
// RGB has been packed, so its float row is no longer live. Reuse that storage for
// alpha narrowing instead of allocating a fourth component buffer or revisiting the image.
Span<L16> packedAlpha = MemoryMarshal.Cast<float, L16>(red)[..width];
HeifSampleConversion.PackL16(alpha, packedAlpha);
for (int x = 0; x < width; x++)
{
packed[x].A = packedAlpha[x].PackedValue;
}
}
if (step.Width == 1)
{
PixelOperations<TPixel>.Instance.FromRgba64(
this.configuration, packed, destination.DangerousGetRowSpan(origin.Y).Slice(origin.X, width));
}
else
{
// Keep the same component narrowing as contiguous output, including alpha, while avoiding
// a second packed pixel buffer for reversed rows and quarter-turn destination columns.
for (int x = 0; x < width; x++)
{
destination.DangerousGetRowSpan(origin.Y)[origin.X] = TPixel.FromRgba64(packed[x]);
origin += step;
}
}
} }
} }
/// <summary> /// <summary>
/// Converts packed image rows to native component planes using pooled planar storage. /// Converts packed destination rows to native component planes using pooled planar storage.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The source pixel type.</typeparam> /// <typeparam name="TPixel">The source pixel type.</typeparam>
/// <typeparam name="TBuffer">The codec adapter exposing the native component planes.</typeparam> /// <typeparam name="TBuffer">The codec adapter exposing the native component planes.</typeparam>
@ -652,7 +855,7 @@ internal static class HeifPlanarColorConverter
private readonly bool isMonochrome; private readonly bool isMonochrome;
/// <summary> /// <summary>
/// Whether source pixels use JPEG-compatible eight-bit RGB plane unpacking. /// Whether source pixels use eight-bit RGB plane unpacking.
/// </summary> /// </summary>
private readonly bool usesByteInput; private readonly bool usesByteInput;
@ -689,7 +892,7 @@ internal static class HeifPlanarColorConverter
} }
/// <summary> /// <summary>
/// Gets a value indicating whether source pixels use JPEG-compatible eight-bit RGB plane unpacking. /// Gets a value indicating whether source pixels use eight-bit RGB plane unpacking.
/// </summary> /// </summary>
public readonly bool UsesByteInput => this.usesByteInput; public readonly bool UsesByteInput => this.usesByteInput;
@ -796,7 +999,7 @@ internal static class HeifPlanarColorConverter
if (this.UsesByteInput) if (this.UsesByteInput)
{ {
// JPEG's planar unpack contract reaches the existing pixel-specific SIMD implementation before // The shared planar unpack contract reaches the existing pixel-specific SIMD implementation before
// the closed H.273 operator transforms the component rows in place. // the closed H.273 operator transforms the component rows in place.
PixelOperations<TPixel>.Instance.UnpackIntoRgbPlanes(luma, chromaBlue, chromaRed, source); PixelOperations<TPixel>.Instance.UnpackIntoRgbPlanes(luma, chromaBlue, chromaRed, source);
this.colorConverter.ConvertFromRgbInPlace(luma, chromaBlue, chromaRed, ByteMaximum); this.colorConverter.ConvertFromRgbInPlace(luma, chromaBlue, chromaRed, ByteMaximum);

396
src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifSampleConversion.cs

@ -21,13 +21,19 @@ internal static class HeifSampleConversion
private const float UShortMaximum = ushort.MaxValue; private const float UShortMaximum = ushort.MaxValue;
/// <summary> /// <summary>
/// Widens reconstructed integer samples into a pooled float component row. /// Widens and normalizes reconstructed integer samples into a pooled float component row.
/// </summary> /// </summary>
/// <typeparam name="TSample">The reconstructed sample type.</typeparam> /// <typeparam name="TSample">The reconstructed sample type.</typeparam>
/// <typeparam name="TLoader">The widening operations for the sample type.</typeparam> /// <typeparam name="TLoader">The widening operations for the sample type.</typeparam>
/// <param name="source">The reconstructed samples.</param> /// <param name="source">The reconstructed samples.</param>
/// <param name="destination">The destination component row.</param> /// <param name="destination">The destination component row.</param>
public static void ConvertSamplesToFloat<TSample, TLoader>(ReadOnlySpan<TSample> source, Span<float> destination) /// <param name="bias">The encoded value corresponding to normalized zero.</param>
/// <param name="scale">The encoded range corresponding to a normalized interval of one.</param>
public static void ConvertSamplesToFloat<TSample, TLoader>(
ReadOnlySpan<TSample> source,
Span<float> destination,
float bias,
float scale)
where TSample : unmanaged where TSample : unmanaged
where TLoader : struct, IHeifSampleConverter<TSample> where TLoader : struct, IHeifSampleConverter<TSample>
{ {
@ -36,46 +42,129 @@ internal static class HeifSampleConversion
int length = destination.Length; int length = destination.Length;
int i = 0; int i = 0;
// Descending vector widths match the JPEG color-converter traversal. A wide-capable CPU processes // Descending vector widths use the widest supported lanes first. A wide-capable CPU processes
// complete wide batches first while short and irregular rows continue through narrower SIMD tails. // complete wide batches first while short and irregular rows continue through narrower SIMD tails.
if (Vector512.IsHardwareAccelerated) if (Vector512.IsHardwareAccelerated)
{ {
Vector512<float> biasVector = Vector512.Create(bias);
Vector512<float> scaleVector = Vector512.Create(scale);
int oneVectorFromEnd = length - Vector512<float>.Count; int oneVectorFromEnd = length - Vector512<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<float>.Count) for (; i <= oneVectorFromEnd; i += Vector512<float>.Count)
{ {
Vector512<float> samples = TLoader.LoadVector512(ref Unsafe.Add(ref sourceBase, i)); Vector512<float> samples = TLoader.LoadVector512(ref Unsafe.Add(ref sourceBase, i));
Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref destinationBase, i)) = samples; Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref destinationBase, i)) = (samples - biasVector) / scaleVector;
} }
} }
if (Vector256.IsHardwareAccelerated) if (Vector256.IsHardwareAccelerated)
{ {
Vector256<float> biasVector = Vector256.Create(bias);
Vector256<float> scaleVector = Vector256.Create(scale);
int oneVectorFromEnd = length - Vector256<float>.Count; int oneVectorFromEnd = length - Vector256<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<float>.Count) for (; i <= oneVectorFromEnd; i += Vector256<float>.Count)
{ {
Vector256<float> samples = TLoader.LoadVector256(ref Unsafe.Add(ref sourceBase, i)); Vector256<float> samples = TLoader.LoadVector256(ref Unsafe.Add(ref sourceBase, i));
Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref destinationBase, i)) = samples; Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref destinationBase, i)) = (samples - biasVector) / scaleVector;
} }
} }
if (Vector128.IsHardwareAccelerated) if (Vector128.IsHardwareAccelerated)
{ {
Vector128<float> biasVector = Vector128.Create(bias);
Vector128<float> scaleVector = Vector128.Create(scale);
int oneVectorFromEnd = length - Vector128<float>.Count; int oneVectorFromEnd = length - Vector128<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count) for (; i <= oneVectorFromEnd; i += Vector128<float>.Count)
{ {
Vector128<float> samples = TLoader.LoadVector128(ref Unsafe.Add(ref sourceBase, i)); Vector128<float> samples = TLoader.LoadVector128(ref Unsafe.Add(ref sourceBase, i));
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref destinationBase, i)) = samples; Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref destinationBase, i)) = (samples - biasVector) / scaleVector;
} }
} }
for (; i < length; i++) for (; i < length; i++)
{ {
Unsafe.Add(ref destinationBase, i) = GetSample(source, i); Unsafe.Add(ref destinationBase, i) = (GetSample(source, i) - bias) / scale;
}
}
/// <summary>
/// Expands horizontally subsampled chroma by repeating each sample at two luma positions.
/// </summary>
/// <typeparam name="TSample">The reconstructed sample type.</typeparam>
/// <typeparam name="TLoader">The widening operations for the sample type.</typeparam>
/// <param name="source">The complete subsampled chroma row.</param>
/// <param name="sourceX">The first luma coordinate of the output window.</param>
/// <param name="destination">The exact output component row.</param>
/// <param name="bias">The encoded chroma value corresponding to normalized zero.</param>
/// <param name="scale">The encoded chroma range.</param>
public static void ReconstructChromaRow<TSample, TLoader>(
ReadOnlySpan<TSample> source,
int sourceX,
Span<float> destination,
float bias,
float scale)
where TSample : unmanaged
where TLoader : struct, IHeifSampleConverter<TSample>
{
ref TSample sourceBase = ref MemoryMarshal.GetReference(source);
ref float destinationBase = ref MemoryMarshal.GetReference(destination);
int sourceIndex = sourceX >> 1;
int i = 0;
// An odd crop origin starts at the second pixel of a replicated pair. Consume that pixel
// before vectorizing complete pairs; the final scalar tail handles an odd right edge.
if ((sourceX & 1) != 0)
{
destination[i++] = (GetSample(source, sourceIndex++) - bias) / scale;
}
if (Vector512.IsHardwareAccelerated)
{
int lastBatch = destination.Length - 32;
for (; i <= lastBatch; i += 32, sourceIndex += 16)
{
Vector512<float> samples = TLoader.LoadVector512(ref Unsafe.Add(ref sourceBase, sourceIndex));
samples = (samples - Vector512.Create(bias)) / Vector512.Create(scale);
// Each four-lane group [a,b,c,d] becomes [a,a,b,b,c,c,d,d]. Keeping groups
// in source order avoids lane-local shuffles interleaving separate pixel groups.
StoreInterleavedChroma(samples.GetLower().GetLower(), samples.GetLower().GetLower(), ref Unsafe.Add(ref destinationBase, i));
StoreInterleavedChroma(samples.GetLower().GetUpper(), samples.GetLower().GetUpper(), ref Unsafe.Add(ref destinationBase, i + 8));
StoreInterleavedChroma(samples.GetUpper().GetLower(), samples.GetUpper().GetLower(), ref Unsafe.Add(ref destinationBase, i + 16));
StoreInterleavedChroma(samples.GetUpper().GetUpper(), samples.GetUpper().GetUpper(), ref Unsafe.Add(ref destinationBase, i + 24));
}
}
if (Vector256.IsHardwareAccelerated)
{
int lastBatch = destination.Length - 16;
for (; i <= lastBatch; i += 16, sourceIndex += 8)
{
Vector256<float> samples = TLoader.LoadVector256(ref Unsafe.Add(ref sourceBase, sourceIndex));
samples = (samples - Vector256.Create(bias)) / Vector256.Create(scale);
StoreInterleavedChroma(samples.GetLower(), samples.GetLower(), ref Unsafe.Add(ref destinationBase, i));
StoreInterleavedChroma(samples.GetUpper(), samples.GetUpper(), ref Unsafe.Add(ref destinationBase, i + 8));
}
}
if (Vector128.IsHardwareAccelerated)
{
int lastBatch = destination.Length - 8;
for (; i <= lastBatch; i += 8, sourceIndex += 4)
{
Vector128<float> samples = TLoader.LoadVector128(ref Unsafe.Add(ref sourceBase, sourceIndex));
samples = (samples - Vector128.Create(bias)) / Vector128.Create(scale);
StoreInterleavedChroma(samples, samples, ref Unsafe.Add(ref destinationBase, i));
}
}
for (; i < destination.Length; i++)
{
destination[i] = (GetSample(source, (sourceX + i) >> 1) - bias) / scale;
} }
} }
/// <summary> /// <summary>
/// Reconstructs one full-width chroma row using the signaled vertical and horizontal sample positions. /// Reconstructs one normalized chroma row using the signaled sample positions.
/// </summary> /// </summary>
/// <typeparam name="TSample">The reconstructed sample type.</typeparam> /// <typeparam name="TSample">The reconstructed sample type.</typeparam>
/// <typeparam name="TLoader">The widening operations for the sample type.</typeparam> /// <typeparam name="TLoader">The widening operations for the sample type.</typeparam>
@ -87,7 +176,9 @@ internal static class HeifSampleConversion
/// <param name="destination">The reconstructed full-width chroma row.</param> /// <param name="destination">The reconstructed full-width chroma row.</param>
/// <param name="scratch0">The first pooled chroma scratch row.</param> /// <param name="scratch0">The first pooled chroma scratch row.</param>
/// <param name="scratch1">The second pooled chroma scratch row.</param> /// <param name="scratch1">The second pooled chroma scratch row.</param>
public static void ReconstructChromaRow<TSample, TLoader>( /// <param name="bias">The encoded chroma value corresponding to normalized zero.</param>
/// <param name="scale">The encoded chroma range.</param>
public static void ReconstructChromaRowBilinear<TSample, TLoader>(
ReadOnlySpan<TSample> row0, ReadOnlySpan<TSample> row0,
ReadOnlySpan<TSample> row1, ReadOnlySpan<TSample> row1,
int y1Weight, int y1Weight,
@ -95,20 +186,15 @@ internal static class HeifSampleConversion
bool isCenteredX, bool isCenteredX,
Span<float> destination, Span<float> destination,
Span<float> scratch0, Span<float> scratch0,
Span<float> scratch1) Span<float> scratch1,
float bias,
float scale)
where TSample : unmanaged where TSample : unmanaged
where TLoader : struct, IHeifSampleConverter<TSample> where TLoader : struct, IHeifSampleConverter<TSample>
{ {
int sourceLength = subX == 0 ? destination.Length : (destination.Length + 1) >> 1; int sourceLength = subX == 0 ? destination.Length : (destination.Length + 1) >> 1;
Span<float> top = scratch0[..sourceLength]; Span<float> top = scratch0[..sourceLength];
ConvertSamplesToFloat<TSample, TLoader>(row0, top); ConvertSamplesToFloat<TSample, TLoader>(row0, top, bias, scale);
if (y1Weight != 0)
{
Span<float> bottom = scratch1[..sourceLength];
ConvertSamplesToFloat<TSample, TLoader>(row1, bottom);
InterpolateChromaRows(top, bottom, y1Weight);
}
if (subX == 0) if (subX == 0)
{ {
@ -116,119 +202,93 @@ internal static class HeifSampleConversion
return; return;
} }
UpsampleChromaHorizontal(top, destination, isCenteredX); ReadOnlySpan<float> bottom = top;
} if (y1Weight != 0)
/// <summary>
/// Interpolates two chroma rows in place using quarter-sample weights.
/// </summary>
/// <param name="top">The upper row, replaced by the interpolated values.</param>
/// <param name="bottom">The lower row.</param>
/// <param name="bottomWeight">The lower-row weight with a denominator of four.</param>
private static void InterpolateChromaRows(Span<float> top, ReadOnlySpan<float> bottom, int bottomWeight)
{
ref float topBase = ref MemoryMarshal.GetReference(top);
ref float bottomBase = ref MemoryMarshal.GetReference(bottom);
int length = top.Length;
int i = 0;
float topWeight = 4 - bottomWeight;
if (Vector512.IsHardwareAccelerated)
{
Vector512<float> topWeightVector = Vector512.Create(topWeight);
Vector512<float> bottomWeightVector = Vector512.Create((float)bottomWeight);
Vector512<float> scale = Vector512.Create(0.25F);
int oneVectorFromEnd = length - Vector512<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<float>.Count)
{
ref Vector512<float> topVector = ref Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref topBase, i));
Vector512<float> bottomVector = Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref bottomBase, i));
topVector = Vector512.MultiplyAddEstimate(bottomWeightVector, bottomVector, topWeightVector * topVector) * scale;
}
}
if (Vector256.IsHardwareAccelerated)
{
Vector256<float> topWeightVector = Vector256.Create(topWeight);
Vector256<float> bottomWeightVector = Vector256.Create((float)bottomWeight);
Vector256<float> scale = Vector256.Create(0.25F);
int oneVectorFromEnd = length - Vector256<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<float>.Count)
{
ref Vector256<float> topVector = ref Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref topBase, i));
Vector256<float> bottomVector = Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref bottomBase, i));
topVector = Vector256.MultiplyAddEstimate(bottomWeightVector, bottomVector, topWeightVector * topVector) * scale;
}
}
if (Vector128.IsHardwareAccelerated)
{ {
Vector128<float> topWeightVector = Vector128.Create(topWeight); Span<float> lower = scratch1[..sourceLength];
Vector128<float> bottomWeightVector = Vector128.Create((float)bottomWeight); ConvertSamplesToFloat<TSample, TLoader>(row1, lower, bias, scale);
Vector128<float> scale = Vector128.Create(0.25F); bottom = lower;
int oneVectorFromEnd = length - Vector128<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count)
{
ref Vector128<float> topVector = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref topBase, i));
Vector128<float> bottomVector = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref bottomBase, i));
topVector = Vector128.MultiplyAddEstimate(bottomWeightVector, bottomVector, topWeightVector * topVector) * scale;
}
} }
for (; i < length; i++) // Normalize each sample before interpolation. Preserve the four products and their
{ // addition order instead of combining duplicate rows or performing two separable blends.
Unsafe.Add(ref topBase, i) = ((Unsafe.Add(ref topBase, i) * topWeight) + (Unsafe.Add(ref bottomBase, i) * bottomWeight)) * 0.25F; // Collapsed vertical boundaries reuse the top row with the same centered weights.
} float closestWeight = y1Weight == 0 ? 0.75F : Math.Max(y1Weight, 4 - y1Weight) * 0.25F;
ReadOnlySpan<float> closest = y1Weight > 2 ? bottom : top;
ReadOnlySpan<float> adjacent = y1Weight > 2 ? top : bottom;
UpsampleChromaHorizontal(closest, adjacent, destination, closestWeight, isCenteredX);
} }
/// <summary> /// <summary>
/// Expands horizontally subsampled chroma to luma width using the selected sample-position rules. /// Blends four normalized chroma samples in closest, horizontal, vertical, diagonal order.
/// </summary> /// </summary>
/// <param name="source">The subsampled chroma values.</param> /// <param name="closest">The vertically closest normalized row.</param>
/// <param name="adjacent">The vertically adjacent normalized row, or the same row at a boundary.</param>
/// <param name="destination">The full-width chroma values.</param> /// <param name="destination">The full-width chroma values.</param>
/// <param name="closestWeight">The weight of the closest row.</param>
/// <param name="isCentered">Whether chroma lies between neighboring luma samples.</param> /// <param name="isCentered">Whether chroma lies between neighboring luma samples.</param>
private static void UpsampleChromaHorizontal(ReadOnlySpan<float> source, Span<float> destination, bool isCentered) private static void UpsampleChromaHorizontal(
ReadOnlySpan<float> closest,
ReadOnlySpan<float> adjacent,
Span<float> destination,
float closestWeight,
bool isCentered)
{ {
ref float sourceBase = ref MemoryMarshal.GetReference(source); ref float closestBase = ref MemoryMarshal.GetReference(closest);
ref float adjacentBase = ref MemoryMarshal.GetReference(adjacent);
ref float destinationBase = ref MemoryMarshal.GetReference(destination); ref float destinationBase = ref MemoryMarshal.GetReference(destination);
int sourceLength = source.Length; int sourceLength = closest.Length;
float adjacentWeight = 1F - closestWeight;
float evenWeight = isCentered ? 0.75F : 1F;
float oddWeight = isCentered ? 0.75F : 0.5F;
float e0 = evenWeight * closestWeight;
float e1 = (1F - evenWeight) * closestWeight;
float e2 = evenWeight * adjacentWeight;
float e3 = (1F - evenWeight) * adjacentWeight;
float o0 = oddWeight * closestWeight;
float o1 = (1F - oddWeight) * closestWeight;
float o2 = oddWeight * adjacentWeight;
float o3 = (1F - oddWeight) * adjacentWeight;
int i = 0; int i = 0;
if (isCentered) if (isCentered)
{ {
// The first centered pair extends the left edge. Interior vectors can then read one real neighbor // Extend the left edge before SIMD loads can address a previous sample.
// on each side and use the exact [1,3]/4 and [3,1]/4 interpolation weights. // Retain duplicate terms: collapsing their weights changes floating-point rounding.
StoreChromaPair(ref destinationBase, 0, source[0], ((3F * source[0]) + source[Math.Min(1, sourceLength - 1)]) * 0.25F, destination.Length); int next = Math.Min(1, sourceLength - 1);
float even = (((closest[0] * e0) + (closest[0] * e1)) + (adjacent[0] * e2)) + (adjacent[0] * e3);
float odd = (((closest[0] * o0) + (closest[next] * o1)) + (adjacent[0] * o2)) + (adjacent[next] * o3);
StoreChromaPair(ref destinationBase, 0, even, odd, destination.Length);
i = 1; i = 1;
} }
if (Vector512.IsHardwareAccelerated) if (Vector512.IsHardwareAccelerated)
{ {
int oneVectorBeforeEnd = sourceLength - Vector512<float>.Count - 1; int oneVectorBeforeEnd = sourceLength - Vector512<float>.Count - 1;
Vector512<float> quarter = Vector512.Create(0.25F); Vector512<float> e0Vector = Vector512.Create(e0);
Vector512<float> half = Vector512.Create(0.5F); Vector512<float> e1Vector = Vector512.Create(e1);
Vector512<float> three = Vector512.Create(3F); Vector512<float> e2Vector = Vector512.Create(e2);
Vector512<float> e3Vector = Vector512.Create(e3);
Vector512<float> o0Vector = Vector512.Create(o0);
Vector512<float> o1Vector = Vector512.Create(o1);
Vector512<float> o2Vector = Vector512.Create(o2);
Vector512<float> o3Vector = Vector512.Create(o3);
for (; i <= oneVectorBeforeEnd; i += Vector512<float>.Count) for (; i <= oneVectorBeforeEnd; i += Vector512<float>.Count)
{ {
Vector512<float> center = Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref sourceBase, i)); // Each lane represents one chroma column. Even lanes use the previous column
Vector512<float> next = Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref sourceBase, i + 1)); // for centered samples; odd lanes use the next. The two rows remain separate
Vector512<float> even; // until the four weighted terms are added, without fused multiply-add rounding.
Vector512<float> odd; int previous = isCentered ? i - 1 : i;
if (isCentered) Vector512<float> center0 = Vector512.LoadUnsafe(ref closestBase, (nuint)i);
{ Vector512<float> center1 = Vector512.LoadUnsafe(ref adjacentBase, (nuint)i);
Vector512<float> previous = Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref sourceBase, i - 1)); Vector512<float> previous0 = Vector512.LoadUnsafe(ref closestBase, (nuint)previous);
even = Vector512.MultiplyAddEstimate(three, center, previous) * quarter; Vector512<float> previous1 = Vector512.LoadUnsafe(ref adjacentBase, (nuint)previous);
odd = Vector512.MultiplyAddEstimate(three, center, next) * quarter; Vector512<float> next0 = Vector512.LoadUnsafe(ref closestBase, (nuint)(i + 1));
} Vector512<float> next1 = Vector512.LoadUnsafe(ref adjacentBase, (nuint)(i + 1));
else Vector512<float> even = (((center0 * e0Vector) + (previous0 * e1Vector)) + (center1 * e2Vector)) + (previous1 * e3Vector);
{ Vector512<float> odd = (((center0 * o0Vector) + (next0 * o1Vector)) + (center1 * o2Vector)) + (next1 * o3Vector);
even = center;
odd = (center + next) * half; StoreInterleavedChroma(even.GetLower().GetLower(), odd.GetLower().GetLower(), ref Unsafe.Add(ref destinationBase, (i * 2) + 0));
}
// Vector512 has no cross-platform unpack helper. The interpolation remains 512-bit; four
// established Vector128 unpack operations only transpose the final even/odd lanes for storage.
StoreInterleavedChroma(even.GetLower().GetLower(), odd.GetLower().GetLower(), ref Unsafe.Add(ref destinationBase, i * 2));
StoreInterleavedChroma(even.GetLower().GetUpper(), odd.GetLower().GetUpper(), ref Unsafe.Add(ref destinationBase, (i * 2) + 8)); StoreInterleavedChroma(even.GetLower().GetUpper(), odd.GetLower().GetUpper(), ref Unsafe.Add(ref destinationBase, (i * 2) + 8));
StoreInterleavedChroma(even.GetUpper().GetLower(), odd.GetUpper().GetLower(), ref Unsafe.Add(ref destinationBase, (i * 2) + 16)); StoreInterleavedChroma(even.GetUpper().GetLower(), odd.GetUpper().GetLower(), ref Unsafe.Add(ref destinationBase, (i * 2) + 16));
StoreInterleavedChroma(even.GetUpper().GetUpper(), odd.GetUpper().GetUpper(), ref Unsafe.Add(ref destinationBase, (i * 2) + 24)); StoreInterleavedChroma(even.GetUpper().GetUpper(), odd.GetUpper().GetUpper(), ref Unsafe.Add(ref destinationBase, (i * 2) + 24));
@ -238,28 +298,30 @@ internal static class HeifSampleConversion
if (Vector256.IsHardwareAccelerated) if (Vector256.IsHardwareAccelerated)
{ {
int oneVectorBeforeEnd = sourceLength - Vector256<float>.Count - 1; int oneVectorBeforeEnd = sourceLength - Vector256<float>.Count - 1;
Vector256<float> quarter = Vector256.Create(0.25F); Vector256<float> e0Vector = Vector256.Create(e0);
Vector256<float> half = Vector256.Create(0.5F); Vector256<float> e1Vector = Vector256.Create(e1);
Vector256<float> three = Vector256.Create(3F); Vector256<float> e2Vector = Vector256.Create(e2);
Vector256<float> e3Vector = Vector256.Create(e3);
Vector256<float> o0Vector = Vector256.Create(o0);
Vector256<float> o1Vector = Vector256.Create(o1);
Vector256<float> o2Vector = Vector256.Create(o2);
Vector256<float> o3Vector = Vector256.Create(o3);
for (; i <= oneVectorBeforeEnd; i += Vector256<float>.Count) for (; i <= oneVectorBeforeEnd; i += Vector256<float>.Count)
{ {
Vector256<float> center = Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref sourceBase, i)); // Each lane represents one chroma column. Even lanes use the previous column
Vector256<float> next = Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref sourceBase, i + 1)); // for centered samples; odd lanes use the next. The two rows remain separate
Vector256<float> even; // until the four weighted terms are added, without fused multiply-add rounding.
Vector256<float> odd; int previous = isCentered ? i - 1 : i;
if (isCentered) Vector256<float> center0 = Vector256.LoadUnsafe(ref closestBase, (nuint)i);
{ Vector256<float> center1 = Vector256.LoadUnsafe(ref adjacentBase, (nuint)i);
Vector256<float> previous = Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref sourceBase, i - 1)); Vector256<float> previous0 = Vector256.LoadUnsafe(ref closestBase, (nuint)previous);
even = Vector256.MultiplyAddEstimate(three, center, previous) * quarter; Vector256<float> previous1 = Vector256.LoadUnsafe(ref adjacentBase, (nuint)previous);
odd = Vector256.MultiplyAddEstimate(three, center, next) * quarter; Vector256<float> next0 = Vector256.LoadUnsafe(ref closestBase, (nuint)(i + 1));
} Vector256<float> next1 = Vector256.LoadUnsafe(ref adjacentBase, (nuint)(i + 1));
else Vector256<float> even = (((center0 * e0Vector) + (previous0 * e1Vector)) + (center1 * e2Vector)) + (previous1 * e3Vector);
{ Vector256<float> odd = (((center0 * o0Vector) + (next0 * o1Vector)) + (center1 * o2Vector)) + (next1 * o3Vector);
even = center;
odd = (center + next) * half; StoreInterleavedChroma(even.GetLower(), odd.GetLower(), ref Unsafe.Add(ref destinationBase, (i * 2) + 0));
}
StoreInterleavedChroma(even.GetLower(), odd.GetLower(), ref Unsafe.Add(ref destinationBase, i * 2));
StoreInterleavedChroma(even.GetUpper(), odd.GetUpper(), ref Unsafe.Add(ref destinationBase, (i * 2) + 8)); StoreInterleavedChroma(even.GetUpper(), odd.GetUpper(), ref Unsafe.Add(ref destinationBase, (i * 2) + 8));
} }
} }
@ -267,55 +329,43 @@ internal static class HeifSampleConversion
if (Vector128.IsHardwareAccelerated) if (Vector128.IsHardwareAccelerated)
{ {
int oneVectorBeforeEnd = sourceLength - Vector128<float>.Count - 1; int oneVectorBeforeEnd = sourceLength - Vector128<float>.Count - 1;
Vector128<float> quarter = Vector128.Create(0.25F); Vector128<float> e0Vector = Vector128.Create(e0);
Vector128<float> half = Vector128.Create(0.5F); Vector128<float> e1Vector = Vector128.Create(e1);
Vector128<float> three = Vector128.Create(3F); Vector128<float> e2Vector = Vector128.Create(e2);
Vector128<float> e3Vector = Vector128.Create(e3);
Vector128<float> o0Vector = Vector128.Create(o0);
Vector128<float> o1Vector = Vector128.Create(o1);
Vector128<float> o2Vector = Vector128.Create(o2);
Vector128<float> o3Vector = Vector128.Create(o3);
for (; i <= oneVectorBeforeEnd; i += Vector128<float>.Count) for (; i <= oneVectorBeforeEnd; i += Vector128<float>.Count)
{ {
Vector128<float> center = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref sourceBase, i)); // Each lane represents one chroma column. Even lanes use the previous column
Vector128<float> next = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref sourceBase, i + 1)); // for centered samples; odd lanes use the next. The two rows remain separate
Vector128<float> even; // until the four weighted terms are added, without fused multiply-add rounding.
Vector128<float> odd; int previous = isCentered ? i - 1 : i;
if (isCentered) Vector128<float> center0 = Vector128.LoadUnsafe(ref closestBase, (nuint)i);
{ Vector128<float> center1 = Vector128.LoadUnsafe(ref adjacentBase, (nuint)i);
Vector128<float> previous = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref sourceBase, i - 1)); Vector128<float> previous0 = Vector128.LoadUnsafe(ref closestBase, (nuint)previous);
even = Vector128.MultiplyAddEstimate(three, center, previous) * quarter; Vector128<float> previous1 = Vector128.LoadUnsafe(ref adjacentBase, (nuint)previous);
odd = Vector128.MultiplyAddEstimate(three, center, next) * quarter; Vector128<float> next0 = Vector128.LoadUnsafe(ref closestBase, (nuint)(i + 1));
} Vector128<float> next1 = Vector128.LoadUnsafe(ref adjacentBase, (nuint)(i + 1));
else Vector128<float> even = (((center0 * e0Vector) + (previous0 * e1Vector)) + (center1 * e2Vector)) + (previous1 * e3Vector);
{ Vector128<float> odd = (((center0 * o0Vector) + (next0 * o1Vector)) + (center1 * o2Vector)) + (next1 * o3Vector);
even = center;
odd = (center + next) * half; StoreInterleavedChroma(even, odd, ref Unsafe.Add(ref destinationBase, (i * 2) + 0));
}
StoreInterleavedChroma(even, odd, ref Unsafe.Add(ref destinationBase, i * 2));
} }
} }
for (; i < sourceLength; i++) for (; i < sourceLength; i++)
{ {
float center = source[i]; int previous = isCentered ? Math.Max(i - 1, 0) : i;
float next = source[Math.Min(i + 1, sourceLength - 1)]; int next = Math.Min(i + 1, sourceLength - 1);
float even = isCentered ? (source[Math.Max(i - 1, 0)] + (3F * center)) * 0.25F : center; float even = (((closest[i] * e0) + (closest[previous] * e1)) + (adjacent[i] * e2)) + (adjacent[previous] * e3);
float odd = isCentered ? ((3F * center) + next) * 0.25F : (center + next) * 0.5F; float odd = (((closest[i] * o0) + (closest[next] * o1)) + (adjacent[i] * o2)) + (adjacent[next] * o3);
StoreChromaPair(ref destinationBase, i * 2, even, odd, destination.Length); StoreChromaPair(ref destinationBase, i * 2, even, odd, destination.Length);
} }
} }
/// <summary>
/// Stores four even chroma lanes interleaved with their four odd lanes.
/// </summary>
/// <param name="even">The even luma-coordinate values.</param>
/// <param name="odd">The odd luma-coordinate values.</param>
/// <param name="destination">The first destination value.</param>
private static void StoreInterleavedChroma(Vector128<float> even, Vector128<float> odd, ref float destination)
{
Vector128<float> lower = Vector128_.UnpackLow(even.AsInt32(), odd.AsInt32()).AsSingle();
Vector128<float> upper = Vector128_.UnpackHigh(even.AsInt32(), odd.AsInt32()).AsSingle();
Unsafe.As<float, Vector128<float>>(ref destination) = lower;
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref destination, Vector128<float>.Count)) = upper;
}
/// <summary> /// <summary>
/// Stores one reconstructed chroma pair without writing beyond an odd-width destination row. /// Stores one reconstructed chroma pair without writing beyond an odd-width destination row.
/// </summary> /// </summary>
@ -333,6 +383,20 @@ internal static class HeifSampleConversion
} }
} }
/// <summary>
/// Stores four even chroma lanes interleaved with their four odd lanes.
/// </summary>
/// <param name="even">The even luma-coordinate values.</param>
/// <param name="odd">The odd luma-coordinate values.</param>
/// <param name="destination">The first destination value.</param>
private static void StoreInterleavedChroma(Vector128<float> even, Vector128<float> odd, ref float destination)
{
Vector128<float> lower = Vector128_.UnpackLow(even.AsInt32(), odd.AsInt32()).AsSingle();
Vector128<float> upper = Vector128_.UnpackHigh(even.AsInt32(), odd.AsInt32()).AsSingle();
Unsafe.As<float, Vector128<float>>(ref destination) = lower;
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref destination, Vector128<float>.Count)) = upper;
}
/// <summary> /// <summary>
/// Deinterleaves high-bit-depth RGB pixels into planar component rows. /// Deinterleaves high-bit-depth RGB pixels into planar component rows.
/// </summary> /// </summary>

44
src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers; using System.Buffers;
using System.Numerics;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
@ -63,36 +64,42 @@ internal static partial class HeifYuvToRgb8Converter
/// <typeparam name="TBuffer">The codec adapter that exposes reconstructed component rows.</typeparam> /// <typeparam name="TBuffer">The codec adapter that exposes reconstructed component rows.</typeparam>
/// <param name="configuration">The configuration used for allocation and pixel conversion.</param> /// <param name="configuration">The configuration used for allocation and pixel conversion.</param>
/// <param name="buffer">The reconstructed component-plane buffer.</param> /// <param name="buffer">The reconstructed component-plane buffer.</param>
/// <param name="image">The destination image frame.</param> /// <param name="destination">The destination pixel region.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param> /// <param name="parameters">The resolved H.273 conversion parameters.</param>
/// <param name="sourceX">The horizontal luma-sample offset of the output window.</param> /// <param name="sourceX">The horizontal luma-sample offset of the output window.</param>
/// <param name="sourceY">The vertical luma-sample offset of the output window.</param> /// <param name="sourceY">The vertical luma-sample offset of the output window.</param>
/// <param name="sourceSize">The extent of the source region before rotation and mirroring.</param>
/// <param name="transform">The rotation and mirroring applied within the destination region.</param>
public static void ConvertFixedPoint<TPixel, TBuffer>( public static void ConvertFixedPoint<TPixel, TBuffer>(
Configuration configuration, Configuration configuration,
TBuffer buffer, TBuffer buffer,
ImageFrame<TPixel> image, Buffer2DRegion<TPixel> destination,
in HeifColorConversionParameters parameters, in HeifColorConversionParameters parameters,
int sourceX, int sourceX,
int sourceY) int sourceY,
Size sourceSize,
HeifPixelTransform transform)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
where TBuffer : struct, IHeifPlanarSampleBuffer<ushort> where TBuffer : struct, IHeifPlanarSampleBuffer<ushort>
{ {
ConversionParameters conversionParameters = new(in parameters); ConversionParameters conversionParameters = new(in parameters);
using IMemoryOwner<byte> componentOwner = configuration.MemoryAllocator.Allocate<byte>(image.Width * 3); Matrix3x2 matrix = transform.GetMatrix(sourceSize);
Size step = new((int)matrix.M11, (int)matrix.M12);
using IMemoryOwner<byte> componentOwner = configuration.MemoryAllocator.Allocate<byte>(sourceSize.Width * 3);
Span<byte> components = componentOwner.GetSpan(); Span<byte> components = componentOwner.GetSpan();
Span<byte> red = components[..image.Width]; Span<byte> red = components[..sourceSize.Width];
Span<byte> green = components.Slice(image.Width, image.Width); Span<byte> green = components.Slice(sourceSize.Width, sourceSize.Width);
Span<byte> blue = components.Slice(image.Width * 2, image.Width); Span<byte> blue = components.Slice(sourceSize.Width * 2, sourceSize.Width);
// The value-type buffer closes the row-access contract at the call site. Constrained calls are therefore // The value-type buffer closes the row-access contract at the call site. Constrained calls are therefore
// devirtualized without boxing while keeping codec-specific buffer ownership outside the color pipeline. // devirtualized without boxing while keeping codec-specific buffer ownership outside the color pipeline.
for (int y = 0; y < image.Height; y++) for (int y = 0; y < sourceSize.Height; y++)
{ {
int lumaY = sourceY + y; int lumaY = sourceY + y;
// The codec boundary validates 4:2:0 crop offsets in complete chroma-sample units. Each native chroma // The codec boundary validates 4:2:0 crop offsets in complete chroma-sample units. Each native chroma
// sample therefore covers one 2x2 luma cell without an alignment branch in the SIMD loop. // sample therefore covers one 2x2 luma cell without an alignment branch in the SIMD loop.
ReadOnlySpan<ushort> luma = buffer.GetLumaRowSpan(lumaY).Slice(sourceX, image.Width); ReadOnlySpan<ushort> luma = buffer.GetLumaRowSpan(lumaY).Slice(sourceX, sourceSize.Width);
ReadOnlySpan<ushort> chromaBlue = buffer.GetChromaBlueRowSpan(lumaY >> 1).Slice(sourceX >> 1); ReadOnlySpan<ushort> chromaBlue = buffer.GetChromaBlueRowSpan(lumaY >> 1).Slice(sourceX >> 1);
ReadOnlySpan<ushort> chromaRed = buffer.GetChromaRedRowSpan(lumaY >> 1).Slice(sourceX >> 1); ReadOnlySpan<ushort> chromaRed = buffer.GetChromaRedRowSpan(lumaY >> 1).Slice(sourceX >> 1);
@ -106,8 +113,23 @@ internal static partial class HeifYuvToRgb8Converter
1, 1,
in conversionParameters); in conversionParameters);
Span<TPixel> destination = image.PixelBuffer.DangerousGetRowSpan(y); if (transform.IsIdentity)
PixelOperations<TPixel>.Instance.PackFromRgbPlanes(red, green, blue, destination); {
Span<TPixel> output = destination.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.PackFromRgbPlanes(red, green, blue, output);
}
else
{
// Noncontiguous output cannot be passed to the planar SIMD packer. Matrix arithmetic above
// remains vectorized; each packed pixel is written once at its final transformed coordinate.
Point point = HeifPixelTransform.Transform(0, y, matrix);
for (int x = 0; x < sourceSize.Width; x++)
{
destination.DangerousGetRowSpan(point.Y)[point.X] = TPixel.FromRgb24(new Rgb24(red[x], green[x], blue[x]));
point += step;
}
}
} }
} }
} }

394
src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs

@ -4,9 +4,13 @@
using System.Buffers; using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Color;
using SixLabors.ImageSharp.Formats.Heif.Components;
using SixLabors.ImageSharp.Formats.Heif.Components.Alpha; using SixLabors.ImageSharp.Formats.Heif.Components.Alpha;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Heif; namespace SixLabors.ImageSharp.Formats.Heif;
@ -15,7 +19,7 @@ namespace SixLabors.ImageSharp.Formats.Heif;
/// Decodes the image items referenced by a HEIF grid derived-image item. /// Decodes the image items referenced by a HEIF grid derived-image item.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam> /// <typeparam name="TPixel">The destination pixel type.</typeparam>
internal sealed class GridHeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>, IHeifAlphaItemDecoder<TPixel> internal sealed class GridHeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>, IHeifAlphaItemDecoder
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
/// <summary> /// <summary>
@ -95,212 +99,128 @@ internal sealed class GridHeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>, IH
/// </summary> /// </summary>
public Heif4CharCode Type => Heif4CharCode.Grid; public Heif4CharCode Type => Heif4CharCode.Grid;
/// <summary> /// <inheritdoc/>
/// Gets the compression method used by the decoded grid tiles. public void DecodeItemData(
/// </summary>
public HeifCompressionMethod CompressionMethod { get; private set; }
/// <summary>
/// Decodes the tiles referenced by a grid derived-image item.
/// </summary>
/// <param name="options">The general options governing the containing HEIF decode.</param>
/// <param name="gridItem">The grid derived-image item.</param>
/// <param name="data">The grid descriptor payload.</param>
/// <param name="colorProfile">The container color description inherited by tiles that do not declare one.</param>
/// <param name="cancellationToken">The token used to cancel between tile payloads.</param>
/// <returns>The image reconstructed from the referenced grid tiles.</returns>
public Image<TPixel> DecodeItemData(
DecoderOptions options, DecoderOptions options,
HeifChromaUpsampling chromaUpsampling,
HeifItem gridItem, HeifItem gridItem,
Span<byte> data, Span<byte> data,
CicpProfile? colorProfile, CicpProfile? colorProfile,
IccProfile? profile,
Av1FrameBuffer<byte>? alphaFrame,
Size alphaOutputSize,
Rectangle alphaRectangle,
bool premultiplied,
Rectangle sourceRectangle,
HeifPixelTransform transform,
Buffer2DRegion<TPixel> destination,
ImageMetadata metadata,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
GridDescriptor descriptor = ParseGridDescriptor(data); GridDescriptor descriptor = ParseGridDescriptor(data);
IReadOnlyList<uint> linked = this.GetLinkedTileIds(gridItem, descriptor); IReadOnlyList<uint> linked = this.GetLinkedTileIds(gridItem, descriptor);
Heif4CharCode tileType = default; Heif4CharCode tileType = default;
Av1CodecConfiguration? av1GridConfiguration = null; Av1CodecConfiguration? av1GridConfiguration = null;
Image<TPixel> result = this.CreateGridResult( Size tileSize = this.items[linked[0]].Extent;
options, ValidateGridCoverage(descriptor, tileSize.Width, tileSize.Height);
descriptor, for (int tileIndex = 0; tileIndex < linked.Count; tileIndex++)
linked[0],
colorProfile,
ref tileType,
ref av1GridConfiguration,
cancellationToken,
out int tileWidth,
out int tileHeight);
try
{ {
for (int tileIndex = 1; tileIndex < linked.Count; tileIndex++) cancellationToken.ThrowIfCancellationRequested();
HeifItem item = this.items[linked[tileIndex]];
ValidateTileConfiguration(item, ref tileType, ref av1GridConfiguration);
if (tileIndex == 0)
{ {
cancellationToken.ThrowIfCancellationRequested(); ValidateGridDimensions(descriptor, tileSize, av1GridConfiguration);
HeifItem item = this.items[linked[tileIndex]]; }
using Image<TPixel> tile = this.DecodeGridTile(
options,
item,
colorProfile,
ref tileType,
ref av1GridConfiguration,
cancellationToken);
Size copySize = GetGridTileCopySize(
descriptor,
tileWidth,
tileHeight,
tileIndex);
if (!IsGridTileExtentValid(tile.Size, copySize, tileWidth, tileHeight))
{
throw new InvalidImageContentException(
$"HEIF image grid tile {item.Id} has dimensions {tile.Size}, which cannot cover its {copySize} grid region.");
}
CopyGridTile(tile, result, descriptor, tileIndex, tileWidth, tileHeight); Size copySize = GetGridTileCopySize(descriptor, tileSize.Width, tileSize.Height, tileIndex);
if (!IsGridTileExtentValid(item.Extent, copySize, tileSize.Width, tileSize.Height))
{
throw new InvalidImageContentException(
$"HEIF image grid tile {item.Id} has dimensions {item.Extent}, which cannot cover its {copySize} grid region.");
} }
return result; IHeifItemDecoder<TPixel> decoder = HeifCompressionFactory.GetDecoder<TPixel>(item.Type)
} ?? throw new ImageFormatException($"The HEIF image grid uses unsupported tile type '{item.Type}'.");
catch
{
result.Dispose();
throw;
}
}
/// <summary> int x = (tileIndex % descriptor.Columns) * tileSize.Width;
/// Decodes the first validated grid tile, establishes the common tile geometry, and creates the output canvas. int y = (tileIndex / descriptor.Columns) * tileSize.Height;
/// </summary>
private Image<TPixel> CreateGridResult(
DecoderOptions options,
in GridDescriptor descriptor,
uint firstTileId,
CicpProfile? colorProfile,
ref Heif4CharCode tileType,
ref Av1CodecConfiguration? av1GridConfiguration,
CancellationToken cancellationToken,
out int tileWidth,
out int tileHeight)
{
cancellationToken.ThrowIfCancellationRequested();
HeifItem item = this.items[firstTileId];
using Image<TPixel> tile = this.DecodeGridTile(
options,
item,
colorProfile,
ref tileType,
ref av1GridConfiguration,
cancellationToken);
tileWidth = tile.Width;
tileHeight = tile.Height;
ValidateGridCoverage(descriptor, tileWidth, tileHeight);
ValidateGridDimensions(descriptor, tile.Size, av1GridConfiguration);
Size copySize = GetGridTileCopySize(descriptor, tileWidth, tileHeight, 0);
if (!IsGridTileExtentValid(tile.Size, copySize, tileWidth, tileHeight))
{
throw new InvalidImageContentException(
$"HEIF image grid tile {item.Id} has dimensions {tile.Size}, which cannot cover its {copySize} grid region.");
}
Image<TPixel> result = new( // Edge tiles retain their coded extent while conversion writes only the visible grid region.
options.Configuration, // The region refers to the final frame, so no decoded tile image or pixel copy is needed.
descriptor.OutputSize.Width, Rectangle tileBounds = new(x, y, copySize.Width, copySize.Height);
descriptor.OutputSize.Height, Rectangle visibleSource = Rectangle.Intersect(tileBounds, sourceRectangle);
tile.Metadata.DeepClone()); if (visibleSource.IsEmpty)
{
continue;
}
try Rectangle relativeSource = new(
{ visibleSource.X - sourceRectangle.X,
CopyGridTile(tile, result, descriptor, 0, tileWidth, tileHeight); visibleSource.Y - sourceRectangle.Y,
return result; visibleSource.Width,
} visibleSource.Height);
catch
{
result.Dispose();
throw;
}
}
/// <summary> Rectangle outputBounds = transform.TransformRectangle(relativeSource, sourceRectangle.Size);
/// Decodes and scales one grid tile while its encoded payload owner is active. Buffer2DRegion<TPixel> tileDestination = destination.GetSubRegion(
/// </summary> outputBounds.X, outputBounds.Y, outputBounds.Width, outputBounds.Height);
private Image<TPixel> DecodeGridTile(
DecoderOptions options,
HeifItem item,
CicpProfile? colorProfile,
ref Heif4CharCode tileType,
ref Av1CodecConfiguration? av1GridConfiguration,
CancellationToken cancellationToken)
{
ValidateTileConfiguration(item, ref tileType, ref av1GridConfiguration);
IHeifItemDecoder<TPixel>? decoder = HeifCompressionFactory.GetDecoder<TPixel>(item.Type)
?? throw new ImageFormatException($"The HEIF image grid uses unsupported tile type '{item.Type}'.");
using IMemoryOwner<byte> itemMemory = this.itemDataReader(item);
this.CompressionMethod = decoder.CompressionMethod;
Image<TPixel> tile = decoder.DecodeItemData(
options,
item,
itemMemory.GetSpan(),
item.CicpProfile ?? colorProfile,
cancellationToken);
try Rectangle tileSource = new(
{ visibleSource.X - x, visibleSource.Y - y, visibleSource.Width, visibleSource.Height);
HeifItemDecoderUtilities.ScaleToItemExtent(tile, item);
return tile;
}
catch
{
// Ownership transfers to the caller only after extent normalization succeeds.
tile.Dispose();
throw;
}
}
/// <summary> // A grid may associate alpha with each coded tile instead of the derived image.
/// Copies one decoded tile into its cropped row-major grid position. // Resolve association at the tile boundary so its RGB is unassociated before ICC conversion.
/// </summary> bool tilePremultiplied = premultiplied;
private static void CopyGridTile( if (alphaFrame is not null && !tilePremultiplied)
Image<TPixel> tile, {
Image<TPixel> result, foreach (HeifItemLink link in this.itemLinks)
in GridDescriptor descriptor, {
int tileIndex, if (link.Type != Heif4CharCode.Prem || link.SourceId != item.Id)
int tileWidth, {
int tileHeight) continue;
{ }
int column = tileIndex % descriptor.Columns;
int row = tileIndex / descriptor.Columns; foreach (uint id in link.DestinationIds)
int destinationX = column * tileWidth; {
int destinationY = row * tileHeight; if (HeifConstants.IsAlphaAuxiliaryType(this.items[id].AuxiliaryType))
int copyWidth = Math.Min(tileWidth, descriptor.OutputSize.Width - destinationX); {
int copyHeight = Math.Min(tileHeight, descriptor.OutputSize.Height - destinationY); tilePremultiplied = true;
ImageFrame<TPixel> source = tile.Frames.RootFrame; break;
ImageFrame<TPixel> destination = result.Frames.RootFrame; }
}
// Copy before disposing this decoded tile, keeping peak tile storage independent of grid cell count.
// The descriptor may crop only the rightmost column and bottom row. if (tilePremultiplied)
for (int y = 0; y < copyHeight; y++) {
{ break;
Span<TPixel> destinationRow = destination.PixelBuffer }
.DangerousGetRowSpan(destinationY + y) }
.Slice(destinationX, copyWidth); }
source.PixelBuffer.DangerousGetRowSpan(y)[..copyWidth].CopyTo(destinationRow); using IMemoryOwner<byte> itemMemory = this.itemDataReader(item);
decoder.DecodeItemData(
options,
chromaUpsampling,
item,
itemMemory.GetSpan(),
item.CicpProfile ?? colorProfile,
profile,
alphaFrame,
alphaOutputSize,
new Rectangle(alphaRectangle.X + relativeSource.X, alphaRectangle.Y + relativeSource.Y, relativeSource.Width, relativeSource.Height),
tilePremultiplied,
tileSource,
transform,
tileDestination,
metadata,
cancellationToken);
} }
} }
/// <inheritdoc/> /// <inheritdoc/>
public void DecodeAlphaItemData( public Av1FrameBuffer<byte> DecodeAlphaItemData(
DecoderOptions options, DecoderOptions options,
HeifItem gridItem, HeifItem gridItem,
Span<byte> data, Span<byte> data,
ImageFrame<TPixel> destination,
Size outputSize,
Rectangle destinationRectangle,
bool premultiplied,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
GridDescriptor descriptor = ParseGridDescriptor(data); GridDescriptor descriptor = ParseGridDescriptor(data);
@ -323,7 +243,7 @@ internal sealed class GridHeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>, IH
uint id = linked[tileIndex]; uint id = linked[tileIndex];
HeifItem item = this.items[id]; HeifItem item = this.items[id];
ValidateTileConfiguration(item, ref tileType, ref av1GridConfiguration); ValidateTileConfiguration(item, ref tileType, ref av1GridConfiguration);
if (HeifCompressionFactory.GetDecoder<TPixel>(item.Type) is not IHeifAlphaItemDecoder<TPixel>) if (HeifCompressionFactory.GetDecoder<TPixel>(item.Type) is not IHeifAlphaItemDecoder)
{ {
throw new ImageFormatException($"The HEIF alpha grid uses unsupported tile type '{item.Type}'."); throw new ImageFormatException($"The HEIF alpha grid uses unsupported tile type '{item.Type}'.");
} }
@ -348,45 +268,101 @@ internal sealed class GridHeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>, IH
ValidateGridDimensions(descriptor, tileSize, av1GridConfiguration); ValidateGridDimensions(descriptor, tileSize, av1GridConfiguration);
int gridWidth = descriptor.OutputSize.Width; // The grid needs one assembled native alpha plane because its tile boundaries need not
int gridHeight = descriptor.OutputSize.Height; // coincide with the color grid. Individual decoded tiles are released after composition.
if (descriptor.OutputSize != outputSize || destinationRectangle.Size != outputSize) Av1FrameBuffer<byte> result;
using (IMemoryOwner<byte> firstMemory = this.itemDataReader(firstItem))
{ {
throw new InvalidImageContentException("The HEIF alpha grid dimensions do not match the color grid dimensions."); Av1HeifItemDecoder<TPixel> firstDecoder = new();
using Av1FrameBuffer<byte> firstFrame = firstDecoder.DecodeAlphaItemData(
options, firstItem, firstMemory.GetSpan(), cancellationToken);
result = Av1FrameBuffer<byte>.CreateAuxiliary(options.Configuration, firstFrame.ColorConfig, descriptor.OutputSize);
try
{
CopyAlphaTile(options.Configuration, firstFrame, result, firstItem.Extent, descriptor, tileSize, 0);
}
catch
{
result.Dispose();
throw;
}
} }
for (int tileIndex = 0; tileIndex < linked.Count; tileIndex++) try
{ {
cancellationToken.ThrowIfCancellationRequested(); for (int tileIndex = 1; tileIndex < linked.Count; tileIndex++)
HeifItem item = this.items[linked[tileIndex]];
IHeifItemDecoder<TPixel>? itemDecoder = HeifCompressionFactory.GetDecoder<TPixel>(item.Type);
if (itemDecoder is not IHeifAlphaItemDecoder<TPixel> decoder)
{ {
throw new InvalidImageContentException($"HEIF alpha grid tile {item.Id} uses unsupported coding format '{item.Type}'."); cancellationToken.ThrowIfCancellationRequested();
HeifItem item = this.items[linked[tileIndex]];
Av1HeifItemDecoder<TPixel> decoder = new();
using IMemoryOwner<byte> itemMemory = this.itemDataReader(item);
using Av1FrameBuffer<byte> frame = decoder.DecodeAlphaItemData(
options, item, itemMemory.GetSpan(), cancellationToken);
CopyAlphaTile(options.Configuration, frame, result, item.Extent, descriptor, tileSize, tileIndex);
} }
using IMemoryOwner<byte> itemMemory = this.itemDataReader(item); return result;
int column = tileIndex % descriptor.Columns; }
int row = tileIndex / descriptor.Columns; catch
int destinationX = destinationRectangle.X + (column * tileSize.Width); {
int destinationY = destinationRectangle.Y + (row * tileSize.Height); result.Dispose();
Size copySize = GetGridTileCopySize( throw;
descriptor, }
tileSize.Width, }
tileSize.Height,
tileIndex);
Rectangle tileDestination = new(destinationX, destinationY, copySize.Width, copySize.Height); /// <summary>
/// Writes one decoded auxiliary tile into the native grid plane.
/// </summary>
/// <param name="configuration">The configuration providing row scratch storage.</param>
/// <param name="source">The decoded tile samples.</param>
/// <param name="destination">The native grid plane.</param>
/// <param name="extent">The tile's presentation extent.</param>
/// <param name="descriptor">The validated grid layout.</param>
/// <param name="tileSize">The nominal grid-cell size.</param>
/// <param name="tileIndex">The tile's row-major index.</param>
private static void CopyAlphaTile(
Configuration configuration,
Av1FrameBuffer<byte> source,
Av1FrameBuffer<byte> destination,
Size extent,
GridDescriptor descriptor,
Size tileSize,
int tileIndex)
{
Size copySize = GetGridTileCopySize(descriptor, tileSize.Width, tileSize.Height, tileIndex);
int left = (tileIndex % descriptor.Columns) * tileSize.Width;
int top = (tileIndex / descriptor.Columns) * tileSize.Height;
using HeifAlphaRowSource rows = Av1YuvConverter.CreateAlphaRowSource(
configuration, source, extent, new Rectangle(Point.Empty, copySize));
decoder.DecodeAlphaItemData( HeifColorConversionParameters parameters = Av1YuvConverter.GetConversionParameters(destination.ColorConfig, destination.ColorConfig.ColorRange, out _);
options, if (destination.BitDepth == Av1BitDepth.EightBit)
item, {
itemMemory.GetSpan(), Av1PlanarSampleBuffer<byte> buffer = new(destination);
destination, for (int y = 0; y < copySize.Height; y++)
item.Extent, {
tileDestination, HeifSampleConversion.WriteSamples<byte, HeifByteSampleConverter>(
premultiplied, rows.ReadRow(y),
cancellationToken); buffer.GetLumaRowSpan(top + y).Slice(left, copySize.Width),
parameters.LumaScale,
parameters.LumaBias,
parameters.LumaSampleMaximum);
}
return;
}
Av1PlanarSampleBuffer<ushort> highBitDepthBuffer = new(destination);
for (int y = 0; y < copySize.Height; y++)
{
HeifSampleConversion.WriteSamples<ushort, HeifUShortSampleConverter>(
rows.ReadRow(y),
highBitDepthBuffer.GetLumaRowSpan(top + y).Slice(left, copySize.Width),
parameters.LumaScale,
parameters.LumaBias,
parameters.LumaSampleMaximum);
} }
} }

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

@ -353,16 +353,6 @@ public enum Heif4CharCode : uint
/// </summary> /// </summary>
Iso8 = 0x69736F38U, Iso8 = 0x69736F38U,
/// <summary>
/// Legacy JPEG coded tile.
/// </summary>
Jpeg = 0x6A706567U,
/// <summary>
/// JPEG image sequence brand.
/// </summary>
Jpgs = 0x6A706773U,
/// <summary> /// <summary>
/// AOMedia Video Coding tile. /// AOMedia Video Coding tile.
/// </summary> /// </summary>

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

@ -74,8 +74,6 @@
"avio", "AVIF intra-only image sequence brand", "avio", "AVIF intra-only image sequence brand",
"msf1", "HEIF image sequence structural brand", "msf1", "HEIF image sequence structural brand",
"iso8", "ISO base media version 8 structural brand", "iso8", "ISO base media version 8 structural brand",
"jpeg", "Legacy JPEG coded tile",
"jpgs", "JPEG image sequence brand",
"av01", "AOMedia Video Coding tile", "av01", "AOMedia Video Coding tile",
"dinf", "Data Information", "dinf", "Data Information",
"grpl", "Group list", "grpl", "Group list",

25
src/ImageSharp/Formats/Heif/HeifChromaUpsampling.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif;
/// <summary>
/// Specifies how subsampled chroma is expanded when decoding HEIF images.
/// </summary>
public enum HeifChromaUpsampling
{
/// <summary>
/// Uses nearest-neighbour sampling for 8-bit source samples and bilinear interpolation for higher bit depths.
/// </summary>
Auto,
/// <summary>
/// Repeats the nearest chroma sample.
/// </summary>
NearestNeighbor,
/// <summary>
/// Interpolates between neighboring chroma samples.
/// </summary>
Bilinear
}

1
src/ImageSharp/Formats/Heif/HeifCompressionFactory.cs

@ -19,7 +19,6 @@ internal static class HeifCompressionFactory
public static IHeifItemDecoder<TPixel>? GetDecoder<TPixel>(Heif4CharCode type) public static IHeifItemDecoder<TPixel>? GetDecoder<TPixel>(Heif4CharCode type)
where TPixel : unmanaged, IPixel<TPixel> => type switch where TPixel : unmanaged, IPixel<TPixel> => type switch
{ {
Heif4CharCode.Jpeg => new JpegHeifItemDecoder<TPixel>(),
Heif4CharCode.Av01 => new Av1HeifItemDecoder<TPixel>(), Heif4CharCode.Av01 => new Av1HeifItemDecoder<TPixel>(),
_ => null _ => null
}; };

20
src/ImageSharp/Formats/Heif/HeifCompressionMethod.cs

@ -1,20 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif;
/// <summary>
/// Identifies the compression method used by a coded image item in a HEIF file.
/// </summary>
public enum HeifCompressionMethod
{
/// <summary>
/// Legacy JPEG coding.
/// </summary>
LegacyJpeg,
/// <summary>
/// AOMedia Video 1 (AV1) coding.
/// </summary>
Av1,
}

17
src/ImageSharp/Formats/Heif/HeifConstants.cs

@ -45,12 +45,6 @@ internal static class HeifConstants
} }
Heif4CharCode majorBrand = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(boxContent); Heif4CharCode majorBrand = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(boxContent);
if (IsUnsupportedSequenceBrand(majorBrand))
{
// A sequence major brand controls the presentation even if a still-image compatible brand is present.
return false;
}
if (IsSupportedSequenceBrand(majorBrand)) if (IsSupportedSequenceBrand(majorBrand))
{ {
fileType = HeifFileType.ImageSequence; fileType = HeifFileType.ImageSequence;
@ -99,8 +93,7 @@ internal static class HeifConstants
/// <returns><see langword="true"/> when the brand identifies a supported still-image container.</returns> /// <returns><see langword="true"/> when the brand identifies a supported still-image container.</returns>
private static bool IsSupportedStillImageBrand(Heif4CharCode brand) private static bool IsSupportedStillImageBrand(Heif4CharCode brand)
=> brand is Heif4CharCode.Mif1 => brand is Heif4CharCode.Mif1
or Heif4CharCode.Avif or Heif4CharCode.Avif;
or Heif4CharCode.Jpeg;
/// <summary> /// <summary>
/// Determines whether <paramref name="brand"/> identifies a supported timed image sequence. /// Determines whether <paramref name="brand"/> identifies a supported timed image sequence.
@ -109,12 +102,4 @@ internal static class HeifConstants
/// <returns><see langword="true"/> when the brand identifies a supported timed image sequence.</returns> /// <returns><see langword="true"/> when the brand identifies a supported timed image sequence.</returns>
private static bool IsSupportedSequenceBrand(Heif4CharCode brand) private static bool IsSupportedSequenceBrand(Heif4CharCode brand)
=> brand is Heif4CharCode.Avis; => brand is Heif4CharCode.Avis;
/// <summary>
/// Determines whether <paramref name="brand"/> requires an image-sequence profile outside the implemented scope.
/// </summary>
/// <param name="brand">The registered file-type brand.</param>
/// <returns><see langword="true"/> when the major brand requires unsupported JPEG sequence support.</returns>
private static bool IsUnsupportedSequenceBrand(Heif4CharCode brand)
=> brand is Heif4CharCode.Jpgs;
} }

16
src/ImageSharp/Formats/Heif/HeifDecoder.cs

@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Heif;
/// <summary> /// <summary>
/// Image decoder for reading HEIF images from a stream. /// Image decoder for reading HEIF images from a stream.
/// </summary> /// </summary>
public sealed class HeifDecoder : ImageDecoder public sealed class HeifDecoder : SpecializedImageDecoder<HeifDecoderOptions>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="HeifDecoder"/> class. /// Initializes a new instance of the <see cref="HeifDecoder"/> class.
@ -28,23 +28,27 @@ public sealed class HeifDecoder : ImageDecoder
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
return new HeifDecoderCore(options).Identify(options.Configuration, stream, cancellationToken); return new HeifDecoderCore(new HeifDecoderOptions { GeneralOptions = options }).Identify(options.Configuration, stream, cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken) protected override Image<TPixel> Decode<TPixel>(HeifDecoderOptions options, Stream stream, CancellationToken cancellationToken)
{ {
Guard.NotNull(options, nameof(options)); Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
HeifDecoderCore decoder = new(options); HeifDecoderCore decoder = new(options);
Image<TPixel> image = decoder.Decode<TPixel>(options.Configuration, stream, cancellationToken); Image<TPixel> image = decoder.Decode<TPixel>(options.GeneralOptions.Configuration, stream, cancellationToken);
ScaleToTargetSize(options, image); ScaleToTargetSize(options.GeneralOptions, image);
return image; return image;
} }
/// <inheritdoc /> /// <inheritdoc />
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) protected override Image Decode(HeifDecoderOptions options, Stream stream, CancellationToken cancellationToken)
=> this.Decode<Rgba32>(options, stream, cancellationToken); => this.Decode<Rgba32>(options, stream, cancellationToken);
/// <inheritdoc/>
protected override HeifDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options)
=> new() { GeneralOptions = options };
} }

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

File diff suppressed because it is too large

18
src/ImageSharp/Formats/Heif/HeifDecoderOptions.cs

@ -0,0 +1,18 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif;
/// <summary>
/// Configuration options for decoding HEIF images.
/// </summary>
public sealed class HeifDecoderOptions : ISpecializedDecoderOptions
{
/// <inheritdoc/>
public DecoderOptions GeneralOptions { get; init; } = new();
/// <summary>
/// Gets the chroma upsampling mode. The default is <see cref="HeifChromaUpsampling.Auto"/>.
/// </summary>
public HeifChromaUpsampling ChromaUpsampling { get; init; }
}

14
src/ImageSharp/Formats/Heif/HeifEncoder.cs

@ -28,12 +28,6 @@ public sealed class HeifEncoder : AnimatedImageEncoder
/// </summary> /// </summary>
private HeifEncodingSpeed speed; private HeifEncodingSpeed speed;
/// <summary>
/// Gets the compression method used for the primary image item.
/// The default is <see cref="HeifCompressionMethod.Av1"/>.
/// </summary>
public HeifCompressionMethod CompressionMethod { get; init; } = HeifCompressionMethod.Av1;
/// <summary> /// <summary>
/// Gets the lossy compression quality, or <see langword="null"/> to use the compression method's default quality. /// Gets the lossy compression quality, or <see langword="null"/> to use the compression method's default quality.
/// Valid values range from 0 for the lowest quality to 100 for the highest quality. A value of 100 does not /// Valid values range from 0 for the lowest quality to 100 for the highest quality. A value of 100 does not
@ -76,8 +70,7 @@ public sealed class HeifEncoder : AnimatedImageEncoder
/// <summary> /// <summary>
/// Gets the encoding effort in the range 0 to 10. A value of 0 selects the fastest encoding and 10 selects the /// Gets the encoding effort in the range 0 to 10. A value of 0 selects the fastest encoding and 10 selects the
/// slowest encoding with the greatest compression effort. The default is 5. Legacy JPEG image items use a fixed /// slowest encoding with the greatest compression effort. The default is 5.
/// encoding effort, so this option does not affect them.
/// </summary> /// </summary>
/// <exception cref="ArgumentException">The effort is outside the range 0 to 10.</exception> /// <exception cref="ArgumentException">The effort is outside the range 0 to 10.</exception>
public int Effort public int Effort
@ -97,7 +90,7 @@ public sealed class HeifEncoder : AnimatedImageEncoder
/// <summary> /// <summary>
/// Gets a value indicating whether the primary and auxiliary alpha images are encoded without loss. When /// Gets a value indicating whether the primary and auxiliary alpha images are encoded without loss. When
/// <see langword="true"/>, <see cref="Quality"/> and <see cref="AlphaQuality"/> do not affect the encoded image. /// <see langword="true"/>, <see cref="Quality"/> and <see cref="AlphaQuality"/> do not affect the encoded image.
/// This option has no effect on legacy JPEG image items. The default is <see langword="false"/>. /// The default is <see langword="false"/>.
/// </summary> /// </summary>
public bool Lossless { get; init; } public bool Lossless { get; init; }
@ -122,8 +115,7 @@ public sealed class HeifEncoder : AnimatedImageEncoder
/// <summary> /// <summary>
/// Gets the encoded precision of each image component, or <see langword="null"/> to use the HEIF metadata bit /// Gets the encoded precision of each image component, or <see langword="null"/> to use the HEIF metadata bit
/// depth. Metadata that does not specify a bit depth defaults to <see cref="HeifBitDepth.Bit8"/>. Legacy JPEG /// depth. Metadata that does not specify a bit depth defaults to <see cref="HeifBitDepth.Bit8"/>.
/// image items are always encoded with <see cref="HeifBitDepth.Bit8"/>.
/// </summary> /// </summary>
public HeifBitDepth? BitDepth { get; init; } public HeifBitDepth? BitDepth { get; init; }

112
src/ImageSharp/Formats/Heif/HeifEncoderCore.cs

@ -8,7 +8,6 @@ using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
@ -106,26 +105,13 @@ internal sealed partial class HeifEncoderCore
Guard.NotNull(image, nameof(image)); Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
switch (this.encoder.CompressionMethod) if (image.Frames.Count > 1 && (image.Width > ushort.MaxValue || image.Height > ushort.MaxValue))
{ {
case HeifCompressionMethod.LegacyJpeg: throw new NotSupportedException("AV1 image-sequence dimensions cannot exceed 65535 pixels.");
break;
case HeifCompressionMethod.Av1:
if (image.Frames.Count > 1)
{
if (image.Width > ushort.MaxValue || image.Height > ushort.MaxValue)
{
throw new NotSupportedException("AV1 image-sequence dimensions cannot exceed 65535 pixels.");
}
}
break;
default:
throw new NotSupportedException($"HEIF compression method '{this.encoder.CompressionMethod}' is not supported.");
} }
using ChunkedMemoryStream compressedPixels = new(this.configuration.MemoryAllocator); using ChunkedMemoryStream compressedPixels = new(this.configuration.MemoryAllocator);
if (this.encoder.CompressionMethod == HeifCompressionMethod.Av1 && image.Frames.Count > 1) if (image.Frames.Count > 1)
{ {
Av1EncodingSettings settings = this.ResolveAv1Encoding(image); Av1EncodingSettings settings = this.ResolveAv1Encoding(image);
bool animateRootFrame = this.encoder.AnimateRootFrame bool animateRootFrame = this.encoder.AnimateRootFrame
@ -200,16 +186,7 @@ internal sealed partial class HeifEncoderCore
List<HeifItem> items = new(); List<HeifItem> items = new();
List<HeifItemLink> links = new(); List<HeifItemLink> links = new();
switch (this.encoder.CompressionMethod) this.CompressAv1Pixels(image, compressedPixels, items, links, cancellationToken);
{
case HeifCompressionMethod.LegacyJpeg:
this.CompressPixels(image, compressedPixels, cancellationToken);
GenerateLegacyJpegItem(image, compressedPixels.Length, items);
break;
case HeifCompressionMethod.Av1:
this.CompressAv1Pixels(image, compressedPixels, items, links, cancellationToken);
break;
}
// Write out the generated header and pixels. // Write out the generated header and pixels.
long metadataBoxOffset = this.WriteFileTypeBox(stream); long metadataBoxOffset = this.WriteFileTypeBox(stream);
@ -218,27 +195,6 @@ internal sealed partial class HeifEncoderCore
stream.Flush(); stream.Flush();
} }
/// <summary>
/// Builds the item declarations and relationships for the encoded image payload.
/// </summary>
/// <typeparam name="TPixel">The source pixel format.</typeparam>
/// <param name="image">The source image.</param>
/// <param name="pixelDataLength">The encoded primary-item payload length.</param>
/// <param name="items">The destination item collection.</param>
private static void GenerateLegacyJpegItem<TPixel>(Image<TPixel> image, long pixelDataLength, List<HeifItem> items)
where TPixel : unmanaged, IPixel<TPixel>
{
HeifItem primaryItem = new(Heif4CharCode.Jpeg, 1u);
primaryItem.DataLocations.Add(new HeifLocation(HeifLocationOffsetOrigin.FileOffset, 0L, 0L, pixelDataLength));
primaryItem.BitsPerPixel = 24;
primaryItem.ChannelCount = 3;
primaryItem.SetExtent(image.Size);
items.Add(primaryItem);
// No item relationship is emitted until the writer has a distinct derived image,
// thumbnail, auxiliary image, or metadata item to reference.
}
/// <summary> /// <summary>
/// Writes an eight-byte ISO BMFF basic box header with a placeholder size. /// Writes an eight-byte ISO BMFF basic box header with a placeholder size.
/// </summary> /// </summary>
@ -290,24 +246,18 @@ internal sealed partial class HeifEncoderCore
{ {
Span<byte> buffer = stackalloc byte[28]; Span<byte> buffer = stackalloc byte[28];
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Ftyp); int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Ftyp);
Heif4CharCode majorBrand = this.encoder.CompressionMethod == HeifCompressionMethod.Av1 BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Avif);
? Heif4CharCode.Avif
: Heif4CharCode.Mif1;
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)majorBrand);
bytesWritten += 4; bytesWritten += 4;
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], 0); BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], 0);
bytesWritten += 4; bytesWritten += 4;
if (majorBrand == Heif4CharCode.Avif)
{ // A still AVIF is also a MIAF image collection, so advertise both structural brands with the codec brand.
// A still AVIF is also a MIAF image collection, so advertise both structural brands with the codec brand. BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Avif);
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Avif); bytesWritten += 4;
bytesWritten += 4; BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Mif1);
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Mif1); bytesWritten += 4;
bytesWritten += 4; BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Miaf);
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Miaf); bytesWritten += 4;
bytesWritten += 4;
}
BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten);
stream.Write(buffer[..bytesWritten]); stream.Write(buffer[..bytesWritten]);
@ -1546,42 +1496,6 @@ internal sealed partial class HeifEncoderCore
throw new ImageFormatException("The Exif profile does not contain a TIFF header."); throw new ImageFormatException("The Exif profile does not contain a TIFF header.");
} }
/// <summary>
/// Encodes the source pixels as the current legacy JPEG item payload.
/// </summary>
/// <typeparam name="TPixel">The source pixel format.</typeparam>
/// <param name="image">The source image.</param>
/// <param name="stream">The destination for the encoded JPEG item bytes.</param>
/// <param name="cancellationToken">The token used to cancel payload encoding.</param>
private void CompressPixels<TPixel>(
Image<TPixel> image,
ChunkedMemoryStream stream,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
JpegColorType colorType = this.encoder.ChromaSubsampling switch
{
null or HeifChromaSubsampling.Yuv420 => JpegColorType.YCbCrRatio420,
HeifChromaSubsampling.Yuv422 => JpegColorType.YCbCrRatio422,
HeifChromaSubsampling.Yuv444 => JpegColorType.YCbCrRatio444,
HeifChromaSubsampling.Monochrome => JpegColorType.Luminance,
_ => throw new NotSupportedException($"HEIF chroma sampling '{this.encoder.ChromaSubsampling}' is not supported.")
};
JpegEncoder encoder = new()
{
// The HEIF quality scale includes zero while the JPEG payload encoder starts at one.
// Map the lowest HEIF setting to the lowest representable JPEG setting.
Quality = this.encoder.Quality == 0 ? 1 : this.encoder.Quality,
ColorType = colorType,
SkipMetadata = this.encoder.SkipMetadata
};
// ImageEncoder is a synchronous contract. Wait for the cancellable JPEG operation so HEIF encoding
// cannot return while its pooled item payload is still being produced.
image.SaveAsJpegAsync(stream, encoder, cancellationToken).GetAwaiter().GetResult();
}
/// <summary> /// <summary>
/// Describes the already-written color and optional alpha extents backing one AV1 image item. /// Describes the already-written color and optional alpha extents backing one AV1 image item.
/// </summary> /// </summary>

33
src/ImageSharp/Formats/Heif/HeifItemDecoderUtilities.cs

@ -1,33 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.Formats.Heif;
/// <summary>
/// Provides shared presentation operations for decoded HEIF image items.
/// </summary>
internal static class HeifItemDecoderUtilities
{
/// <summary>
/// Scales a decoded image to the spatial extent associated with its image item.
/// </summary>
/// <typeparam name="TPixel">The decoded pixel format.</typeparam>
/// <param name="image">The decoded image.</param>
/// <param name="item">The image item that defines the presented spatial extent.</param>
public static void ScaleToItemExtent<TPixel>(Image<TPixel> image, HeifItem item)
where TPixel : unmanaged, IPixel<TPixel>
{
Size extent = item.Extent;
if (extent == default || (image.Width == extent.Width && image.Height == extent.Height))
{
return;
}
// libavif applies box filtering when coded dimensions differ from an item's ispe dimensions. Reusing the
// same ImageSharp resampler keeps direct images and grid tiles on one presentation path.
image.Mutate(context => context.Resize(extent.Width, extent.Height, KnownResamplers.Box));
}
}

6
src/ImageSharp/Formats/Heif/HeifMetadata.cs

@ -24,7 +24,6 @@ public class HeifMetadata : IFormatMetadata<HeifMetadata>
/// <param name="other">The metadata to create an instance from.</param> /// <param name="other">The metadata to create an instance from.</param>
private HeifMetadata(HeifMetadata other) private HeifMetadata(HeifMetadata other)
{ {
this.CompressionMethod = other.CompressionMethod;
this.BitDepth = other.BitDepth; this.BitDepth = other.BitDepth;
this.IsMonochrome = other.IsMonochrome; this.IsMonochrome = other.IsMonochrome;
this.HasAlpha = other.HasAlpha; this.HasAlpha = other.HasAlpha;
@ -38,11 +37,6 @@ public class HeifMetadata : IFormatMetadata<HeifMetadata>
this.NominalDiffuseWhite = other.NominalDiffuseWhite; this.NominalDiffuseWhite = other.NominalDiffuseWhite;
} }
/// <summary>
/// Gets or sets the compression method used for the primary frame.
/// </summary>
public HeifCompressionMethod CompressionMethod { get; set; } = HeifCompressionMethod.LegacyJpeg;
/// <summary> /// <summary>
/// Gets or sets the encoded precision of each color component. The default is <see cref="HeifBitDepth.Bit8"/>. /// Gets or sets the encoded precision of each color component. The default is <see cref="HeifBitDepth.Bit8"/>.
/// </summary> /// </summary>

113
src/ImageSharp/Formats/Heif/HeifPixelTransform.cs

@ -0,0 +1,113 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif;
/// <summary>
/// Maps cropped source pixels to their rotated and mirrored destination coordinates.
/// </summary>
internal readonly struct HeifPixelTransform
{
private readonly Matrix3x2 orientation;
/// <summary>
/// Initializes a new instance of the <see cref="HeifPixelTransform"/> struct.
/// </summary>
/// <param name="rotation">The counter-clockwise quarter-turn count.</param>
/// <param name="mirrorAxis">The optional destination mirror axis.</param>
public HeifPixelTransform(byte rotation, byte? mirrorAxis)
{
this.orientation = Matrix3x2.CreateRotation(-rotation * MathF.PI / 2);
this.orientation *= Matrix3x2.CreateScale(mirrorAxis == 1 ? -1 : 1, mirrorAxis == 0 ? -1 : 1);
}
/// <summary>
/// Gets a value indicating whether source rows map directly to destination rows.
/// </summary>
public bool IsIdentity => this.orientation == default || this.orientation.IsIdentity;
/// <summary>
/// Gets the destination extent for a cropped source extent.
/// </summary>
/// <param name="sourceSize">The cropped source extent.</param>
/// <returns>The destination extent.</returns>
public Size GetDestinationSize(Size sourceSize)
=> Rectangle.Round(Rectangle.Transform(new Rectangle(Point.Empty, sourceSize), this.GetMatrix(sourceSize))).Size;
/// <summary>
/// Finds the source rectangle corresponding to a destination window.
/// </summary>
/// <param name="destination">The window in transformed coordinates.</param>
/// <param name="sourceSize">The complete cropped source extent.</param>
/// <returns>The source window before rotation and mirroring.</returns>
public Rectangle GetSourceRectangle(Rectangle destination, Size sourceSize)
{
Matrix3x2.Invert(this.GetMatrix(sourceSize), out Matrix3x2 inverse);
return Rectangle.Round(Rectangle.Transform(destination, inverse));
}
/// <summary>
/// Maps one source pixel to the destination.
/// </summary>
/// <param name="x">The source column relative to the crop.</param>
/// <param name="y">The source row relative to the crop.</param>
/// <param name="matrix">The matrix resolved once for the source region.</param>
/// <returns>The destination pixel coordinate.</returns>
public static Point Transform(int x, int y, Matrix3x2 matrix)
{
Vector2 point = Vector2.Transform(new Vector2(x + 0.5F, y + 0.5F), matrix);
return new Point((int)MathF.Floor(point.X), (int)MathF.Floor(point.Y));
}
/// <summary>
/// Maps a source rectangle to its exact destination rectangle.
/// </summary>
/// <param name="source">The rectangle relative to the cropped source.</param>
/// <param name="sourceSize">The complete cropped source extent.</param>
/// <returns>The destination rectangle.</returns>
public Rectangle TransformRectangle(Rectangle source, Size sourceSize)
{
return Rectangle.Round(Rectangle.Transform(source, this.GetMatrix(sourceSize)));
}
/// <summary>
/// Writes a converted source row into its destination coordinates.
/// </summary>
/// <typeparam name="TPixel">The packed pixel type.</typeparam>
/// <param name="row">The converted source row.</param>
/// <param name="y">The source row index relative to the crop.</param>
/// <param name="matrix">The matrix resolved once for the source region.</param>
/// <param name="destination">The exact destination region.</param>
public static void WriteRow<TPixel>(ReadOnlySpan<TPixel> row, int y, Matrix3x2 matrix, Buffer2DRegion<TPixel> destination)
where TPixel : unmanaged
{
// The matrix's first basis vector is the integer destination step for one source column.
// Quarter turns and mirrors require no matrix operations inside the pixel loop.
Point first = Transform(0, y, matrix);
int stepX = (int)matrix.M11;
int stepY = (int)matrix.M12;
for (int x = 0; x < row.Length; x++)
{
destination.DangerousGetRowSpan(first.Y)[first.X] = row[x];
first.X += stepX;
first.Y += stepY;
}
}
/// <summary>
/// Gets the transform translated into the positive destination bounds.
/// </summary>
/// <param name="sourceSize">The source extent.</param>
/// <returns>The transform of rectangle edges and pixel centers.</returns>
public Matrix3x2 GetMatrix(Size sourceSize)
{
Matrix3x2 matrix = this.IsIdentity ? Matrix3x2.Identity : this.orientation;
RectangleF bounds = Rectangle.Transform(new Rectangle(Point.Empty, sourceSize), matrix);
matrix.Translation = new Vector2(-bounds.X, -bounds.Y);
return matrix;
}
}

32
src/ImageSharp/Formats/Heif/IHeifItemDecoder.cs

@ -1,7 +1,11 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Heif; namespace SixLabors.ImageSharp.Formats.Heif;
@ -18,26 +22,40 @@ internal interface IHeifItemDecoder<TPixel>
/// </summary> /// </summary>
public Heif4CharCode Type { get; } public Heif4CharCode Type { get; }
/// <summary>
/// Gets the compression method used by the image item.
/// </summary>
public HeifCompressionMethod CompressionMethod { get; }
/// <summary> /// <summary>
/// Decodes the compressed payload of an image item. /// Decodes the compressed payload of an image item.
/// </summary> /// </summary>
/// <param name="options">The general options governing the containing HEIF decode.</param> /// <param name="options">The general options governing the containing HEIF decode.</param>
/// <param name="chromaUpsampling">The chroma reconstruction mode.</param>
/// <param name="item">The HEIF item whose encoded payload is being decoded.</param> /// <param name="item">The HEIF item whose encoded payload is being decoded.</param>
/// <param name="data">The encoded image payload.</param> /// <param name="data">The encoded image payload.</param>
/// <param name="colorProfile"> /// <param name="colorProfile">
/// The container color description that overrides matching color information in the encoded image payload. /// The container color description that overrides matching color information in the encoded image payload.
/// </param> /// </param>
/// <param name="profile">The source ICC profile selected for conversion, or null to preserve source colors.</param>
/// <param name="alphaFrame">The native auxiliary plane, or null for an opaque image.</param>
/// <param name="alphaOutputSize">The complete color extent covered by the auxiliary plane.</param>
/// <param name="alphaRectangle">The matching region within the auxiliary presentation.</param>
/// <param name="premultiplied">Whether source RGB is associated with alpha.</param>
/// <param name="sourceRectangle">The source area of interest in luma-sample coordinates.</param>
/// <param name="transform">The rotation and mirroring applied within the destination region.</param>
/// <param name="destination">The destination pixel region.</param>
/// <param name="metadata">The metadata receiving the decoded image properties.</param>
/// <param name="cancellationToken">The token used to cancel the payload decode.</param> /// <param name="cancellationToken">The token used to cancel the payload decode.</param>
/// <returns>The decoded image.</returns> public void DecodeItemData(
public Image<TPixel> DecodeItemData(
DecoderOptions options, DecoderOptions options,
HeifChromaUpsampling chromaUpsampling,
HeifItem item, HeifItem item,
Span<byte> data, Span<byte> data,
CicpProfile? colorProfile, CicpProfile? colorProfile,
IccProfile? profile,
Av1FrameBuffer<byte>? alphaFrame,
Size alphaOutputSize,
Rectangle alphaRectangle,
bool premultiplied,
Rectangle sourceRectangle,
HeifPixelTransform transform,
Buffer2DRegion<TPixel> destination,
ImageMetadata metadata,
CancellationToken cancellationToken); CancellationToken cancellationToken);
} }

52
src/ImageSharp/Formats/Heif/JpegHeifItemDecoder.cs

@ -1,52 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Heif;
/// <summary>
/// Decodes a single JPEG-coded HEIF image item.
/// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
internal class JpegHeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
/// <summary>
/// Gets the JPEG-coded image item type.
/// </summary>
public Heif4CharCode Type => Heif4CharCode.Jpeg;
/// <summary>
/// Gets the legacy JPEG compression method.
/// </summary>
public HeifCompressionMethod CompressionMethod => HeifCompressionMethod.LegacyJpeg;
/// <summary>
/// Decodes the encoded JPEG payload of an image item.
/// </summary>
/// <param name="options">The general options governing the containing HEIF decode.</param>
/// <param name="item">The HEIF item whose encoded payload is being decoded.</param>
/// <param name="data">The encoded JPEG payload.</param>
/// <param name="colorProfile">The container color description associated with the image item.</param>
/// <param name="cancellationToken">The token used to cancel the payload decode.</param>
/// <returns>The decoded image.</returns>
public unsafe Image<TPixel> DecodeItemData(
DecoderOptions options,
HeifItem item,
Span<byte> data,
CicpProfile? colorProfile,
CancellationToken cancellationToken)
{
// The JPEG decoder owns the payload's JPEG color coding. The containing decoder attaches HEIF CICP as
// presentation metadata after payload decode, so it must not be mistaken for JPEG component-transform syntax.
fixed (byte* dataPointer = data)
{
using UnmanagedMemoryStream stream = new(dataPointer, data.Length);
using JpegDecoderCore decoder = new(new JpegDecoderOptions { GeneralOptions = options });
return decoder.Decode<TPixel>(options.Configuration, stream, cancellationToken);
}
}
}

16
src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs

@ -60,23 +60,15 @@ internal class FlipProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source) protected override void OnFrameApply(ImageFrame<TPixel> source)
=> Apply(this.definition.FlipMode, source, this.Configuration);
/// <summary>
/// Applies an exact axis-aligned reflection to an existing frame.
/// </summary>
/// <param name="flipMode">The reflection direction.</param>
/// <param name="source">The frame modified in place.</param>
/// <param name="configuration">The configuration controlling row parallelism and scratch allocation.</param>
internal static void Apply(FlipMode flipMode, ImageFrame<TPixel> source, Configuration configuration)
{ {
switch (flipMode) switch (this.definition.FlipMode)
{ {
// No default needed as we have already set the pixels.
case FlipMode.Vertical: case FlipMode.Vertical:
FlipX(source.PixelBuffer, configuration); FlipX(source.PixelBuffer, this.Configuration);
break; break;
case FlipMode.Horizontal: case FlipMode.Horizontal:
FlipY(source, configuration); FlipY(source, this.Configuration);
break; break;
} }
} }

33
src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs

@ -103,52 +103,25 @@ internal class RotateProcessor<TPixel> : AffineTransformProcessor<TPixel>
if (MathF.Abs(degrees - 90) < Constants.Epsilon) if (MathF.Abs(degrees - 90) < Constants.Epsilon)
{ {
ApplyQuarterTurn(RotateMode.Rotate90, source, destination, configuration); Rotate90(source, destination, configuration);
return true; return true;
} }
if (MathF.Abs(degrees - 180) < Constants.Epsilon) if (MathF.Abs(degrees - 180) < Constants.Epsilon)
{ {
ApplyQuarterTurn(RotateMode.Rotate180, source, destination, configuration); Rotate180(source, destination, configuration);
return true; return true;
} }
if (MathF.Abs(degrees - 270) < Constants.Epsilon) if (MathF.Abs(degrees - 270) < Constants.Epsilon)
{ {
ApplyQuarterTurn(RotateMode.Rotate270, source, destination, configuration); Rotate270(source, destination, configuration);
return true; return true;
} }
return false; return false;
} }
/// <summary>
/// Applies an exact quarter-turn rotation between already allocated frames.
/// </summary>
/// <param name="rotation">The clockwise quarter-turn rotation.</param>
/// <param name="source">The source frame.</param>
/// <param name="destination">The destination frame with the rotated dimensions.</param>
/// <param name="configuration">The configuration controlling row parallelism.</param>
internal static void ApplyQuarterTurn(
RotateMode rotation,
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination,
Configuration configuration)
{
switch (rotation)
{
case RotateMode.Rotate90:
Rotate90(source, destination, configuration);
break;
case RotateMode.Rotate180:
Rotate180(source, destination, configuration);
break;
case RotateMode.Rotate270:
Rotate270(source, destination, configuration);
break;
}
}
/// <summary> /// <summary>
/// Rotates the image 180 degrees clockwise at the centre point. /// Rotates the image 180 degrees clockwise at the centre point.
/// </summary> /// </summary>

17
tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ColorConversionBenchmarks.cs

@ -2,9 +2,11 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Heif;
using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Color; using SixLabors.ImageSharp.Formats.Heif.Av1.Color;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif; namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif;
@ -113,7 +115,20 @@ public class Av1ColorConversionBenchmarks
{ {
Av1FrameBuffer<byte> frameBuffer = this.frameBuffer; Av1FrameBuffer<byte> frameBuffer = this.frameBuffer;
Image<Rgb48> destination = this.destination; Image<Rgb48> destination = this.destination;
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame); Av1YuvConverter.ConvertToRgb(
Configuration.Default,
frameBuffer,
new Rectangle(Point.Empty, destination.Frames.RootFrame.Size),
destination.Frames.RootFrame.PixelBuffer.GetRegion(),
destination.Frames.RootFrame.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
frameBuffer.ColorConfig.ColorRange);
return destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(Height - 1)[Width - 1]; return destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(Height - 1)[Width - 1];
} }

208
tests/ImageSharp.Benchmarks/Codecs/Heif/Av1SequenceEncoderBenchmarks.cs

@ -1,208 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Heif;
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.Formats.Heif.Components;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests;
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif;
/// <summary>
/// Measures encoding a photographic sequence with fractional motion at the interpolation-search effort boundaries.
/// </summary>
[MemoryDiagnoser]
public class Av1SequenceEncoderBenchmarks
{
/// <summary>
/// The number of displayed pictures in each independently encoded sequence.
/// </summary>
private const int FrameCount = 3;
/// <summary>
/// The native AV1 quantizer index corresponding to libaom's public constant-quality level 30.
/// </summary>
private const int QIndex = 120;
/// <summary>
/// The fixed native speed baseline, independent of ImageSharp's effort scale.
/// </summary>
private const int NativeCpuUsed = 6;
/// <summary>
/// The native public quantizer corresponding to <see cref="QIndex"/>, also used for both rate-control bounds.
/// </summary>
private const int NativeQuality = 30;
private Image<Rgb24> sequence;
private Configuration configuration;
private ObuColorConfig colorConfig;
private MemoryStream output;
private string outputDirectory;
/// <summary>
/// Gets or sets the square frame dimension.
/// </summary>
[Params(256, 512)]
public int Dimension { get; set; }
/// <summary>
/// Gets or sets the effort controlling fixed, common switchable, or independently switchable filters.
/// </summary>
[Params(7, 8, 9)]
public int Effort { get; set; }
/// <summary>
/// Prepares identical photographic RGB frames and a planar source file for checking reconstructed output quality.
/// </summary>
[GlobalSetup]
public void Setup()
{
this.configuration = Configuration.Default.Clone();
this.configuration.MaxDegreeOfParallelism = 1;
this.colorConfig = new ObuColorConfig
{
BitDepth = Av1BitDepth.EightBit,
IsColorDescriptionPresent = true,
ColorPrimaries = ObuColorPrimaries.Bt601,
TransferCharacteristics = ObuTransferCharacteristics.Bt601,
MatrixCoefficients = ObuMatrixCoefficients.Bt601,
ColorRange = true,
SubSamplingX = true,
SubSamplingY = true,
ChromaSamplePosition = ObuChromoSamplePosition.Unknown
};
this.output = new MemoryStream();
this.outputDirectory = TestEnvironment.CreateOutputDirectory("Heif", "Av1", nameof(Av1SequenceEncoderBenchmarks));
string inputPath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Png.Bike);
using Image<Rgb24> photograph = Image.Load<Rgb24>(inputPath);
// Leave a source margin for the half-pixel translations. Resampling is setup work, not encoder time;
// every invocation consumes the same three images rather than repeatedly translating a previous result.
photograph.Mutate(context => context.Resize(new ResizeOptions
{
Size = new Size(this.Dimension + FrameCount, this.Dimension + FrameCount),
Mode = ResizeMode.Crop
}));
Rectangle sourceBounds = new(0, 0, photograph.Width, photograph.Height);
Size targetSize = new(this.Dimension, this.Dimension);
this.sequence = photograph.Clone(context => context.Crop(new Rectangle(Point.Empty, targetSize)));
for (int frameIndex = 1; frameIndex < FrameCount; frameIndex++)
{
Matrix3x2 translation = Matrix3x2.CreateTranslation(-0.5F * frameIndex, -0.5F * frameIndex);
using Image<Rgb24> translated = photograph.Clone(context =>
context.Transform(sourceBounds, translation, targetSize, KnownResamplers.Bicubic));
this.sequence.Frames.AddFrame(translated.Frames.RootFrame);
}
// Export the production-converted source planes only for checking reconstructed output quality.
// Neither timed encoder reads this file: both convert the original RGB frames during each operation.
using Av1EncoderFrameBuffer<byte> planar = new(this.configuration, this.Dimension, this.Dimension, 8, Av1ColorFormat.Yuv420, 0, 0, lumaBorder: 64);
using FileStream raw = File.Create(Path.Combine(this.outputDirectory, $"bike-{this.Dimension}-3frames.source.yuv"));
foreach (ImageFrame<Rgb24> frame in this.sequence.Frames)
{
Av1FrameEncoder.PrepareSource(this.configuration, frame, planar.Frame, this.colorConfig);
for (int planeIndex = 0; planeIndex < this.colorConfig.PlaneCount; planeIndex++)
{
Buffer2DRegion<byte> plane = planar.Frame.View.GetPlane((Av1Plane)planeIndex);
for (int y = 0; y < plane.Height; y++)
{
raw.Write(plane.DangerousGetRowSpan(y));
}
}
}
}
/// <summary>
/// Encodes one key picture and two dependent pictures, returning the complete OBU payload length.
/// </summary>
/// <returns>The encoded sequence length.</returns>
[Benchmark]
public long ImageSharp()
{
this.output.SetLength(0);
using Av1FrameEncoder.SequenceEncoder encoder = Av1FrameEncoder.CreateColorSequenceEncoder(
this.configuration,
this.Dimension,
this.Dimension,
this.colorConfig,
QIndex,
this.Effort,
speed: HeifEncodingSpeed.Level0);
// One operation owns the real sequence lifetime: allocation, conversion, key/inter coding, and disposal.
// The caller's destination is reused, excluding filesystem and MemoryStream growth from steady-state timing.
encoder.EncodeKeyFrame(this.sequence.Frames.RootFrame, this.output);
for (int frameIndex = 1; frameIndex < FrameCount; frameIndex++)
{
encoder.EncodeInterFrame(this.sequence.Frames[frameIndex], this.output);
}
return this.output.Length;
}
/// <summary>
/// Encodes the same RGB sequence with current-main libaom, including conversion, allocation, output, and disposal.
/// </summary>
/// <returns>The encoded sequence length.</returns>
[Benchmark(Baseline = true)]
public long Libaom()
{
// cpu-used is a separate speed scale, not an ImageSharp effort mapping. Keep the reference at
// good-quality speed six while comparing the three managed interpolation-search boundaries.
this.output.SetLength(0);
using LibaomBenchmarkEncoder encoder = LibaomBenchmarkEncoder.Open(this.Dimension, this.Dimension, NativeQuality, NativeCpuUsed);
using Av1EncoderFrameBuffer<byte> planar = new(this.configuration, this.Dimension, this.Dimension, 8, Av1ColorFormat.Yuv420, 0, 0, lumaBorder: 64);
using Av1FrameEncoder.Av1EncoderConversionWorkspace conversion = new(this.configuration, this.Dimension, this.colorConfig, false, false);
Rectangle bounds = new(0, 0, this.Dimension, this.Dimension);
for (int frameIndex = 0; frameIndex < FrameCount; frameIndex++)
{
// Conversion belongs inside both measured paths. Reuse the same row workspace and SIMD converter
// as the managed sequence encoder, writing directly into the planes passed to native libaom.
conversion.Convert<Rgb24, Av1EncoderFrame<byte>.PlanarView, byte, HeifByteSampleConverter>(
this.configuration, this.sequence.Frames[frameIndex], bounds, planar.Frame.View);
encoder.Encode(planar.Frame, frameIndex, this.output);
}
encoder.Finish(this.output);
return this.output.Length;
}
/// <summary>
/// Retains the measured managed encoder output and releases the input images and destination stream.
/// </summary>
[GlobalCleanup(Target = nameof(ImageSharp))]
public void CleanupImageSharp() => this.Cleanup($"bike-{this.Dimension}-q{QIndex}-effort{this.Effort}.obu");
/// <summary>
/// Retains the measured reference encoder output and releases the input images and destination stream.
/// </summary>
[GlobalCleanup(Target = nameof(Libaom))]
public void CleanupLibaom() => this.Cleanup($"bike-{this.Dimension}-q{QIndex}-libaom-cpu{NativeCpuUsed}.obu");
/// <summary>
/// Writes the measured payload without another encoding pass and releases the shared benchmark resources.
/// </summary>
/// <param name="outputName">The codec-specific payload file name.</param>
private void Cleanup(string outputName)
{
// Output validation and quality measurement use the actual measured payload, with no encode or decode
// hidden inside the timed operation and no file-sized ToArray copy.
using FileStream encoded = File.Create(Path.Combine(this.outputDirectory, outputName));
this.output.Position = 0;
this.output.CopyTo(encoded);
this.output.Dispose();
this.sequence.Dispose();
}
}

118
tests/ImageSharp.Benchmarks/Codecs/Heif/README.md

@ -1,7 +1,23 @@
# AV1 sequence encoder comparison # AV1 encoder and decoder comparisons
The 2026-09-05 measurements below are historical results from earlier trees. The takeover source audit has not
accepted encoder or decoder completeness. The first decoder comparison following that audit's corrections ran
on 2026-09-07; its current results and limitations are recorded in the implementation plan.
Encoder comparisons require identical source samples and explicitly reconciled settings, with a maximum
component difference of one unit. Report component maxima and counts exceeding one.
Decoder comparisons for identical bitstreams must be byte exact: maximum error zero and zero differing samples.
Historical Y/U/V maxima of 40/30/53 fail that requirement; their exceedance counts were not recorded.
Agreement between two decoders on the same bitstream is a separate claim from agreement between two encoders.
`Av1SequenceEncoderBenchmarks` compares the managed AV1 sequence encoder with an optimized build of official libaom `main`. `Av1SequenceEncoderBenchmarks` compares the managed AV1 sequence encoder with an optimized build of official libaom `main`.
The native adapter belongs only to this benchmark project. ImageSharp production code remains fully managed. The native adapter is temporary local comparison tooling already present in this project. It is excluded from the
codec deliverable and future checkpoint commits. ImageSharp production code remains fully managed.
`Av1DecoderBenchmarks` compares complete AV1 elementary-stream decoding to `Rgb48` against that same optimized native build.
It includes decoder construction, parsing, reconstruction, output allocation, color conversion, and disposal on both sides.
File reads and correctness checks are setup work. HEIF container parsing is outside this elementary-stream comparison.
Native output planes are borrowed only during synchronous conversion through the existing HEIF converter; no plane copy or per-row interop call is added.
Setup compares every packed RGB sample with ImageSharp and fails before timing on any disagreement.
## Measurement boundary ## Measurement boundary
@ -15,6 +31,7 @@ MemoryStream capacity is retained between operations for both methods. These are
Do not compare them with `aomenc`'s internal encode-time report, which excludes the conversion boundary. Do not compare them with `aomenc`'s internal encode-time report, which excludes the conversion boundary.
The source is the existing `TestImages.Png.Bike` photograph. Frames one and two are independently translated by half a pixel and one pixel on both axes. The source is the existing `TestImages.Png.Bike` photograph. Frames one and two are independently translated by half a pixel and one pixel on both axes.
Encoder setup also checks all three encoded photographic frames with independent ImageSharp and libaom decoder contexts, including their retained references.
The parameter matrix contains 256x256 and 512x512 frames at ImageSharp efforts seven, eight, and nine. The parameter matrix contains 256x256 and 512x512 frames at ImageSharp efforts seven, eight, and nine.
Input is full-range BT.601, eight-bit 4:2:0; each sequence contains one key frame and two dependent frames. Input is full-range BT.601, eight-bit 4:2:0; each sequence contains one key frame and two dependent frames.
Both codecs use one coding thread. Libaom uses good-quality mode, no lookahead, disabled automatic key frames, and `cpu-used=6`. Both codecs use one coding thread. Libaom uses good-quality mode, no lookahead, disabled automatic key frames, and `cpu-used=6`.
@ -135,3 +152,100 @@ Final OBU SHA-256 values:
The final Release benchmark build has zero errors and 39 existing benchmark warnings outside the new files. The final Release benchmark build has zero errors and 39 existing benchmark warnings outside the new files.
Roslyn compiler and analyzer diagnostics contain no errors or warnings in the new benchmark files. Roslyn compiler and analyzer diagnostics contain no errors or warnings in the new benchmark files.
No production file changed in this benchmark checkpoint; the preceding 2,524-case encoder/entropy verification remains the latest production test run. No production file changed in this benchmark checkpoint; the preceding 2,524-case encoder/entropy verification remains the latest production test run.
## Decoder baseline and sparse-transform checkpoint: 2026-09-05
Run the two existing conformance inputs with the same in-process toolchain:
```powershell
dotnet $benchmarkAssembly --inProcess --job Dry --filter '*Av1DecoderBenchmarks*' `
--stopOnFirstError --noOverwrite --artifacts artifacts/BenchmarkDotNet/av1-decoder-rgb-dry
dotnet $benchmarkAssembly --inProcess --job Short --filter '*Av1DecoderBenchmarks*' `
--stopOnFirstError --noOverwrite --artifacts artifacts/BenchmarkDotNet/av1-decoder-rgb-short
```
Both complete `Rgb48` outputs match libaom exactly. Initial warmed measurements were:
| Input | ImageSharp | Libaom |
| --- | ---: | ---: |
| `libavif-kodim23-8b.bit` | 15.130 ms | 5.423 ms |
| `libavif-cosmos1650-10b.bit` | 27.655 ms | 10.246 ms |
The lossy inverse dispatcher previously ignored EOB and transformed every coefficient, including DC-only blocks.
The first sparse path now evaluates the two DC cosine stages once, preserving rectangular normalization, axis rounding, input clamps, and final clipping.
The existing reconstruction output operators handle Vector512, Vector256, Vector128, and scalar stores without an extra sample buffer.
After this change the short measurements were 14.658/5.416 ms for the eight-bit pair and 26.722/9.992 ms for the ten-bit pair.
These small changes are not a statistically established decoder speedup; the approximately 2.7x gap remains open.
The encoder's measured output at this checkpoint remains byte-identical to the baseline hash above.
Evidence: `artifacts/BenchmarkDotNet/av1-decoder-rgb-short-20260905` and `artifacts/BenchmarkDotNet/av1-dc-short-20260905/20260905-134350`.
DC regression cases compare the sparse and full transforms for every size, both destination layouts, signed rounding boundaries, clipping, and untouched row padding.
The final focused screening/DC/native-profile/moving-color set passes 25 cases in each of the normal, AVX512-disabled, AVX-disabled, and all-intrinsics-disabled VSTest processes.
Reports are under `artifacts/TestResults/av1-screen-20260905`.
The matching optimized current-main decoder also reproduces every native Y/U/V sample in all twelve two-frame moving-color streams regenerated by this tree.
## Historical intra screening experiment: 2026-09-05
The current decoder-only comparison after source-led corrections is recorded at the end of this document.
The experimental fixed 8x8 luma search imported libaom's unnormalized Hadamard magnitude and 1.5x threshold
without its full candidate ordering, top-ranked list, neighbor/quantizer policy, or surrounding search controller.
The implementation reuses block scratch and existing vectorized tensor/transpose APIs, retains Int32 precision for twelve-bit residuals, and omits coefficient permutations irrelevant to SATD.
Lossless search is unchanged. Screening for larger blocks, top-ranked candidate pruning, transform bounds, and winner refinement remain open.
The first three-iteration run measured 1,437.53 ms versus 82.34 ms, with considerable run variance.
The managed output is 11.577 KiB at Y/U/V PSNR 38.942/38.613/32.755 dB and aggregate 37.069 dB.
Libaom remains 8.272 KiB at aggregate 38.942 dB. Both decode to three complete frames.
This experiment is not an accepted improvement: aggregate PSNR fell 0.055 dB and the payload grew slightly.
Neither its isolated primitive tests nor these timing results validate the imported pruning policy.
The exact photographic sequence also passes independent packed-RGB decoder comparison before either timed method runs.
A repeat with five warmup iterations and ten measurement iterations gives 1,363.09 ms for ImageSharp and 71.73 ms for libaom.
The payload and quality are unchanged from the screening result above. The native baseline is stable relative to the original run;
the managed reduction is about 34%, but the remaining encoding gap is still approximately 19x.
Evidence: `artifacts/BenchmarkDotNet/av1-screen-verified-short-20260905`.
The final managed OBU SHA-256 is `CCA72021F1AC0BBEA5E69F536589F859B76D2CF293ECD6BA86A644A57B56686E`.
The focused production run passes 84 screening/DC/superblock cases and 165 frame/public encoder cases, with no failures or skips.
Release builds report zero errors, 1,009 existing test warnings, and 39 existing benchmark warnings; the changed source files have no build warnings.
One initial narrower-tier VSTest launch aborted before tests because the inherited environment contained both `Path` and `PATH`.
Subsequent serialized runs use a case-insensitively deduplicated child-process environment. No machine or user environment setting was changed.
The independent Hadamard matrix oracle and zero-allocation test pass at all tested hardware tiers; no golden output was changed to accept a mismatch.
## Decoder source-correction measurement: 2026-09-07
The initial takeover investigation, reconstruction/filter/scratch corrections, and final production verification
preceded this measurement. No encoder timing or separately encoded output comparison was run.
The existing decoder benchmark uses the elementary-stream boundary described above, with one coding thread,
construction and disposal per operation, and independently allocated RGB48 output. Both paths use ImageSharp's
color converter, so this is not an independent color-conversion oracle or a HEIF container-load measurement.
| Input | Encoded size | RGB48 output size | ImageSharp mean | Native mean | ImageSharp managed allocation |
| --- | ---: | ---: | ---: | ---: | ---: |
| Kodak, 768x512, 8-bit 4:2:0 | 20.264 KiB | 2.250 MiB | 13.296 ms | 5.099 ms | 715.97 KiB |
| Cosmos, 1024x428, 10-bit 4:4:4 | 36.298 KiB | 2.508 MiB | 25.419 ms | 9.448 ms | 797.24 KiB |
Setup verifies all 2,494,464 packed RGB48 components exactly: maximum error zero, no differing samples, and
zero errors above one. Fresh optimized native decoding also matches the 1,904,640 retained native plane samples
used by the current conformance tests, with the same zero-error counts. Decoder agreement on these inputs does
not establish complete decoder conformance or separately encoded output parity.
Immediately before the CDF-width correction, the same Short job reported 13.739/5.133 ms for Kodak and
24.848/9.263 ms for Cosmos. Replacing 32-bit CDF threshold storage with its required 16-bit representation
reduced managed allocation by about 217 KiB per decode. These three-iteration timing samples do not establish
a speedup. The current managed/native gap remains approximately 2.61x and 2.69x. The native allocation footprint
is not included in the managed allocation column.
The benchmark uses BenchmarkDotNet 0.15.8, its existing in-process toolchain, .NET 11.0.0-preview.7.26381.103,
x64 RyuJIT x86-64-v4, three warmups and three measured iterations. CPU-model lookup and power-plan changes
were denied by the environment. Current 99.9% error half-widths are 2.800/1.959 ms for Kodak and
26.309/6.052 ms for Cosmos (managed/native); the latter results have substantial variance.
The optimized reference remains `d565eec60f084421fa34fc0534b760c6452b6a6c` with the Release configuration above.
The existing local adapter SHA-256 is `B4FCB9028C15F0F7961A4CEF77FDFF265A11720132F5B23562673E5A65635B23`.
All 9,997 AV1 and selected HEIF production cases pass after the final CDF edit, and fresh comparisons preserve
exact agreement across the established 8,521,435-sample restoration, film-grain, and moving-color corpus.
These are bounded verification results, not codec completion.
Reports remain outside the repository under `D:\GitHub\ynse01\av1-takeover-20260905`:
`decoder-current-short\20260907-011627` before CDF narrowing and `decoder-current-short\20260907-012459`
afterward, with matching Dry jobs and `decoder-benchmark-inputs.json`. No native material was staged or committed.

6
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs

@ -69,7 +69,7 @@ public class Av1CoefficientsEntropyTests
} }
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog); TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
Assert.Equal(allocation.AllocationId, returned.AllocationId); Assert.Equal(allocation.HashCodeOfBuffer, returned.HashCodeOfBuffer);
} }
[Theory] [Theory]
@ -398,7 +398,7 @@ public class Av1CoefficientsEntropyTests
} }
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog); TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
Assert.Equal(allocation.AllocationId, returned.AllocationId); Assert.Equal(allocation.HashCodeOfBuffer, returned.HashCodeOfBuffer);
} }
[Fact] [Fact]
@ -458,7 +458,7 @@ public class Av1CoefficientsEntropyTests
} }
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog); TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
Assert.Equal(allocation.AllocationId, returned.AllocationId); Assert.Equal(allocation.HashCodeOfBuffer, returned.HashCodeOfBuffer);
} }
[Fact] [Fact]

39
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs

@ -5,7 +5,6 @@ using System.Buffers;
using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames;
@ -82,13 +81,11 @@ public class Av1CompoundBlockDecoderTests
partitionInfo.ComputeBoundaryOffsets(sequenceHeader, frameHeader, tileInfo); partitionInfo.ComputeBoundaryOffsets(sequenceHeader, frameHeader, tileInfo);
partitionInfo.PopulateModeInfoNeighbors(sequenceHeader.ColorConfig); partitionInfo.PopulateModeInfoNeighbors(sequenceHeader.ColorConfig);
using Av1LoopFilterContext loopFilterContext = new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
using IMemoryOwner<short> workspace = frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(sequenceHeader)); using IMemoryOwner<short> workspace = frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(sequenceHeader));
Av1BlockDecoder decoder = new( Av1BlockDecoder decoder = new(
sequenceHeader, sequenceHeader,
frameHeader, frameHeader,
frameBuffer, frameBuffer,
loopFilterContext,
referenceFrames, referenceFrames,
workspace.Memory); workspace.Memory);
decoder.UpdateSuperblock(superblockInfo); decoder.UpdateSuperblock(superblockInfo);
@ -202,15 +199,11 @@ public class Av1CompoundBlockDecoderTests
modeInfo.InterpolationFilters.Clear(); modeInfo.InterpolationFilters.Clear();
modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1);
using Av1LoopFilterContext loopFilterContext =
new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
using IMemoryOwner<short> workspace = frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(sequenceHeader)); using IMemoryOwner<short> workspace = frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(sequenceHeader));
Av1BlockDecoder decoder = new( Av1BlockDecoder decoder = new(
sequenceHeader, sequenceHeader,
frameHeader, frameHeader,
frameBuffer, frameBuffer,
loopFilterContext,
referenceFrames, referenceFrames,
workspace.Memory); workspace.Memory);
@ -322,15 +315,11 @@ public class Av1CompoundBlockDecoderTests
modeInfo.InterpolationFilters.Fill(Av1InterpolationFilter.Bilinear); modeInfo.InterpolationFilters.Fill(Av1InterpolationFilter.Bilinear);
modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1);
using Av1LoopFilterContext loopFilterContext =
new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
using IMemoryOwner<short> workspace = frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(sequenceHeader)); using IMemoryOwner<short> workspace = frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(sequenceHeader));
Av1BlockDecoder decoder = new( Av1BlockDecoder decoder = new(
sequenceHeader, sequenceHeader,
frameHeader, frameHeader,
frameBuffer, frameBuffer,
loopFilterContext,
referenceFrames, referenceFrames,
workspace.Memory); workspace.Memory);
@ -449,15 +438,11 @@ public class Av1CompoundBlockDecoderTests
modeInfo.InterpolationFilters.Clear(); modeInfo.InterpolationFilters.Clear();
modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1);
using Av1LoopFilterContext loopFilterContext =
new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
using IMemoryOwner<short> workspace = frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(sequenceHeader)); using IMemoryOwner<short> workspace = frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(sequenceHeader));
Av1BlockDecoder decoder = new( Av1BlockDecoder decoder = new(
sequenceHeader, sequenceHeader,
frameHeader, frameHeader,
frameBuffer, frameBuffer,
loopFilterContext,
referenceFrames, referenceFrames,
workspace.Memory); workspace.Memory);
@ -541,15 +526,11 @@ public class Av1CompoundBlockDecoderTests
modeInfo.InterpolationFilters.Clear(); modeInfo.InterpolationFilters.Clear();
modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1);
using Av1LoopFilterContext loopFilterContext =
new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
using IMemoryOwner<short> workspace = frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(sequenceHeader)); using IMemoryOwner<short> workspace = frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(sequenceHeader));
Av1BlockDecoder decoder = new( Av1BlockDecoder decoder = new(
sequenceHeader, sequenceHeader,
frameHeader, frameHeader,
frameBuffer, frameBuffer,
loopFilterContext,
referenceFrames, referenceFrames,
workspace.Memory); workspace.Memory);
@ -625,15 +606,11 @@ public class Av1CompoundBlockDecoderTests
current.SetTransformUnitCount(Av1PlaneType.Y, 1); current.SetTransformUnitCount(Av1PlaneType.Y, 1);
frameInfo.UpdateModeInfo(current, superblockInfo); frameInfo.UpdateModeInfo(current, superblockInfo);
using Av1LoopFilterContext loopFilterContext =
new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
using IMemoryOwner<short> workspace = frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(sequenceHeader)); using IMemoryOwner<short> workspace = frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(sequenceHeader));
Av1BlockDecoder decoder = new( Av1BlockDecoder decoder = new(
sequenceHeader, sequenceHeader,
frameHeader, frameHeader,
frameBuffer, frameBuffer,
loopFilterContext,
referenceFrames, referenceFrames,
workspace.Memory); workspace.Memory);
@ -722,15 +699,11 @@ public class Av1CompoundBlockDecoderTests
current.SetTransformUnitCount(Av1PlaneType.Uv, 1); current.SetTransformUnitCount(Av1PlaneType.Uv, 1);
frameInfo.UpdateModeInfo(current, superblockInfo); frameInfo.UpdateModeInfo(current, superblockInfo);
using Av1LoopFilterContext loopFilterContext =
new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
using IMemoryOwner<short> workspace = frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(sequenceHeader)); using IMemoryOwner<short> workspace = frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(sequenceHeader));
Av1BlockDecoder decoder = new( Av1BlockDecoder decoder = new(
sequenceHeader, sequenceHeader,
frameHeader, frameHeader,
frameBuffer, frameBuffer,
loopFilterContext,
referenceFrames, referenceFrames,
workspace.Memory); workspace.Memory);
@ -979,15 +952,11 @@ public class Av1CompoundBlockDecoderTests
using Av1FrameInfo frameInfo = new(sequenceHeader); using Av1FrameInfo frameInfo = new(sequenceHeader);
Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty);
superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0);
using Av1LoopFilterContext loopFilterContext =
new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
using IMemoryOwner<short> workspace = frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(sequenceHeader)); using IMemoryOwner<short> workspace = frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(sequenceHeader));
Av1BlockDecoder decoder = new( Av1BlockDecoder decoder = new(
sequenceHeader, sequenceHeader,
frameHeader, frameHeader,
frameBuffer, frameBuffer,
loopFilterContext,
referenceFrames, referenceFrames,
workspace.Memory); workspace.Memory);
@ -1375,15 +1344,11 @@ public class Av1CompoundBlockDecoderTests
using Av1FrameInfo frameInfo = new(sequenceHeader); using Av1FrameInfo frameInfo = new(sequenceHeader);
Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty);
superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0);
using Av1LoopFilterContext loopFilterContext =
new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
using IMemoryOwner<short> workspace = frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(sequenceHeader)); using IMemoryOwner<short> workspace = frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(sequenceHeader));
Av1BlockDecoder decoder = new( Av1BlockDecoder decoder = new(
sequenceHeader, sequenceHeader,
frameHeader, frameHeader,
frameBuffer, frameBuffer,
loopFilterContext,
referenceFrames, referenceFrames,
workspace.Memory); workspace.Memory);
@ -1585,15 +1550,11 @@ public class Av1CompoundBlockDecoderTests
modeInfo.InterpolationFilters.Clear(); modeInfo.InterpolationFilters.Clear();
modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1);
using Av1LoopFilterContext loopFilterContext =
new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
using IMemoryOwner<short> workspace = frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(sequenceHeader)); using IMemoryOwner<short> workspace = frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(sequenceHeader));
Av1BlockDecoder decoder = new( Av1BlockDecoder decoder = new(
sequenceHeader, sequenceHeader,
frameHeader, frameHeader,
frameBuffer, frameBuffer,
loopFilterContext,
referenceFrames, referenceFrames,
workspace.Memory); workspace.Memory);

12
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1DeblockingFilterTests.cs

@ -196,12 +196,14 @@ public class Av1DeblockingFilterTests
{ {
Skip = true, Skip = true,
YMode = mode, YMode = mode,
TransformSize = Av1TransformSize.Size8x8,
}; };
Av1BlockModeInfo rightModeInfo = new(Av1BlockSize.Block16x8, new Point(4, 0)) Av1BlockModeInfo rightModeInfo = new(Av1BlockSize.Block16x8, new Point(4, 0))
{ {
Skip = true, Skip = true,
YMode = mode, YMode = mode,
TransformSize = Av1TransformSize.Size8x8,
}; };
leftModeInfo.ReferenceFrames[0] = referenceFrame; leftModeInfo.ReferenceFrames[0] = referenceFrame;
@ -209,14 +211,6 @@ public class Av1DeblockingFilterTests
frameInfo.UpdateModeInfo(leftModeInfo, superblock); frameInfo.UpdateModeInfo(leftModeInfo, superblock);
frameInfo.UpdateModeInfo(rightModeInfo, superblock); frameInfo.UpdateModeInfo(rightModeInfo, superblock);
using Av1LoopFilterContext loopFilterContext =
new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader);
loopFilterContext.SetTransformSize(Av1Plane.Y, Point.Empty, Av1TransformSize.Size8x8);
loopFilterContext.SetTransformSize(Av1Plane.Y, new Point(2, 0), Av1TransformSize.Size8x8);
loopFilterContext.SetTransformSize(Av1Plane.Y, new Point(4, 0), Av1TransformSize.Size8x8);
loopFilterContext.SetTransformSize(Av1Plane.Y, new Point(6, 0), Av1TransformSize.Size8x8);
byte[] expected = new byte[width * height]; byte[] expected = new byte[width * height];
for (int row = 0; row < height; row++) for (int row = 0; row < height; row++)
{ {
@ -234,7 +228,7 @@ public class Av1DeblockingFilterTests
ApplyReference(expected, true, edge, 8, limit, boundaryLimit, highEdgeVarianceThreshold, 8); ApplyReference(expected, true, edge, 8, limit, boundaryLimit, highEdgeVarianceThreshold, 8);
ApplyReference(expected, true, (4 * width) + edge, 8, limit, boundaryLimit, highEdgeVarianceThreshold, 8); ApplyReference(expected, true, (4 * width) + edge, 8, limit, boundaryLimit, highEdgeVarianceThreshold, 8);
Av1LoopFilterDecoder decoder = new(sequenceHeader, frameHeader, frameInfo, frameBuffer, loopFilterContext); Av1LoopFilterDecoder decoder = new(sequenceHeader, frameHeader, frameInfo, frameBuffer);
decoder.DecodeFrame(); decoder.DecodeFrame();
for (int row = 0; row < height; row++) for (int row = 0; row < height; row++)

445
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs

@ -5,6 +5,7 @@ using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Heif; using SixLabors.ImageSharp.Formats.Heif;
using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Color;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
@ -159,7 +160,22 @@ public class Av1EncoderFrameTests
effort: 0); effort: 0);
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using Image<L8> decoded = decoder.Decode<L8>(stream.ToArray()); using Av1FrameBuffer<byte> decodedPlanes = decoder.DecodeFrameBuffer(stream.ToArray(), null, null, out _);
using Image<L8> decoded = new(Configuration.Default, decodedPlanes.Width, decodedPlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
decodedPlanes,
decoded.Bounds,
decoded.Frames.RootFrame.PixelBuffer.GetRegion(decoded.Bounds),
decoded.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
decodedPlanes.ColorConfig.ColorRange);
Assert.Equal(2, decoder.FrameHeader.TilesInfo.TileColumnCount); Assert.Equal(2, decoder.FrameHeader.TilesInfo.TileColumnCount);
Assert.Equal(1, decoder.FrameHeader.TilesInfo.TileRowCount); Assert.Equal(1, decoder.FrameHeader.TilesInfo.TileRowCount);
@ -236,21 +252,24 @@ public class Av1EncoderFrameTests
effort: 5); effort: 5);
byte[] payload = stream.ToArray(); byte[] payload = stream.ToArray();
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
string contentName = hasGradient ? "gradient" : "constant";
int bitCount = bitDepth.GetBitCount();
string colorName = colorFormat.ToString()[3..];
string fileName = $"encoder-frame-{width}x{height}-{bitCount}b-{colorName}-{contentName}.obu";
File.WriteAllBytes(Path.Combine(outputDirectory, fileName), payload);
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload); using Av1FrameBuffer<byte> decodedPlanes = decoder.DecodeFrameBuffer(payload, null, null, out _);
using Image<Rgba32> decoded = new(Configuration.Default, decodedPlanes.Width, decodedPlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
decodedPlanes,
decoded.Bounds,
decoded.Frames.RootFrame.PixelBuffer.GetRegion(decoded.Bounds),
decoded.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
decodedPlanes.ColorConfig.ColorRange);
Assert.Equal(width, decoded.Width); Assert.Equal(width, decoded.Width);
Assert.Equal(height, decoded.Height); Assert.Equal(height, decoded.Height);
@ -332,7 +351,23 @@ public class Av1EncoderFrameTests
ObuSequenceHeader encodedHeader = encoder.SequenceHeader; ObuSequenceHeader encodedHeader = encoder.SequenceHeader;
byte[] payload = stream.ToArray(); byte[] payload = stream.ToArray();
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload); using Av1FrameBuffer<byte> decodedPlanes = decoder.DecodeFrameBuffer(payload, null, null, out _);
using Image<Rgba32> decoded = new(Configuration.Default, decodedPlanes.Width, decodedPlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
decodedPlanes,
decoded.Bounds,
decoded.Frames.RootFrame.PixelBuffer.GetRegion(decoded.Bounds),
decoded.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
decodedPlanes.ColorConfig.ColorRange);
ObuSequenceHeader decodedHeader = decoder.SequenceHeader; ObuSequenceHeader decodedHeader = decoder.SequenceHeader;
Assert.False(encodedHeader.IsStillPicture); Assert.False(encodedHeader.IsStillPicture);
@ -427,14 +462,6 @@ public class Av1EncoderFrameTests
effort, effort,
speed); speed);
string outputFolder = width == 23 ? nameof(SequenceEncoderPreservesNativeColorPlanesWithSubpixelMotion)
: nameof(SequenceEncoderPreservesNativeColorPlanesAcrossMotionCostRefreshRows);
string outputDirectory = TestEnvironment.CreateOutputDirectory("Heif", "Av1", outputFolder);
string outputName = $"{bitDepth.GetBitCount()}-{colorFormat}-effort{effort}-speed{(int)speed}";
using FileStream output = File.Create(Path.Combine(outputDirectory, outputName + ".obu"));
using BinaryWriter rawOutput = new(File.Create(Path.Combine(outputDirectory, outputName + ".managed.yuv")));
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using MemoryStream sample = new(); using MemoryStream sample = new();
for (int frameIndex = 0; frameIndex < 3; frameIndex++) for (int frameIndex = 0; frameIndex < 3; frameIndex++)
@ -466,8 +493,6 @@ public class Av1EncoderFrameTests
encoder.EncodeInterFrame(source.Frames.RootFrame, sample); encoder.EncodeInterFrame(source.Frames.RootFrame, sample);
} }
sample.Position = 0;
sample.CopyTo(output);
decoder.DecodeSequenceReference(sample.ToArray(), null, null); decoder.DecodeSequenceReference(sample.ToArray(), null, null);
Assert.True(Assert.IsType<ObuSequenceHeader>(decoder.SequenceHeader).EnableIntraEdgeFilter); Assert.True(Assert.IsType<ObuSequenceHeader>(decoder.SequenceHeader).EnableIntraEdgeFilter);
Av1FrameBuffer<byte> decoded = Assert.IsType<Av1FrameBuffer<byte>>(decoder.FrameBuffer); Av1FrameBuffer<byte> decoded = Assert.IsType<Av1FrameBuffer<byte>>(decoder.FrameBuffer);
@ -484,33 +509,6 @@ public class Av1EncoderFrameTests
Assert.False(mode.Skip); Assert.False(mode.Skip);
} }
} }
for (int planeIndex = 0; planeIndex < 3; planeIndex++)
{
Av1Plane plane = (Av1Plane)planeIndex;
int subsamplingX = plane == Av1Plane.Y || !colorConfig.SubSamplingX ? 0 : 1;
int subsamplingY = plane == Av1Plane.Y || !colorConfig.SubSamplingY ? 0 : 1;
int planeHeight = (height + subsamplingY) >> subsamplingY;
if (bitDepth == Av1BitDepth.EightBit)
{
Buffer2DRegion<byte> planeSamples = decoded.DeriveBlockPointer(plane, subsamplingX, subsamplingY);
for (int y = 0; y < planeHeight; y++)
{
rawOutput.Write(planeSamples.DangerousGetRowSpan(y));
}
}
else
{
for (int y = 0; y < planeHeight; y++)
{
foreach (ushort value in decoded.GetHighBitDepthRowSpan(plane, y, subsamplingX, subsamplingY))
{
// Raw high-bit-depth output uses explicit little-endian samples on every host.
rawOutput.Write(value);
}
}
}
}
} }
ObuFrameHeader frameHeader = Assert.IsType<ObuFrameHeader>(decoder.FrameHeader); ObuFrameHeader frameHeader = Assert.IsType<ObuFrameHeader>(decoder.FrameHeader);
@ -570,21 +568,19 @@ public class Av1EncoderFrameTests
encoder.EncodeKeyFrame(source.Frames.RootFrame, firstSample); encoder.EncodeKeyFrame(source.Frames.RootFrame, firstSample);
encoder.EncodeInterFrame(source.Frames.RootFrame, secondSample); encoder.EncodeInterFrame(source.Frames.RootFrame, secondSample);
// Retain the exact two-sample elementary stream for independent reference-decoder acceptance.
string outputDirectory = TestEnvironment.CreateOutputDirectory("Heif", "Av1", nameof(this.SequenceEncoderUsesRetainedReconstructionForInterFrame));
using (FileStream output = File.Create(Path.Combine(outputDirectory, $"effort-{effort}.obu")))
{
firstSample.Position = 0;
firstSample.CopyTo(output);
secondSample.Position = 0;
secondSample.CopyTo(output);
}
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using ImageFrame<Rgba32> decodedFirst = decoder.DecodeSequenceFrame<Rgba32>( using ImageFrame<Rgba32> decodedFirst = new(Configuration.Default, Width, Height);
decoder.DecodeSequenceFrame(
firstSample.ToArray(), firstSample.ToArray(),
null, null,
null); null,
decodedFirst.Size,
decodedFirst.Bounds,
decodedFirst.PixelBuffer.GetRegion(decodedFirst.Bounds),
default,
null,
null,
false);
Av1FrameInfo firstFrameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo); Av1FrameInfo firstFrameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
foreach (Av1BlockModeInfo mode in firstFrameInfo.GetModeInfos(Point.Empty, firstFrameInfo.GetModeInfoCount(Point.Empty))) foreach (Av1BlockModeInfo mode in firstFrameInfo.GetModeInfos(Point.Empty, firstFrameInfo.GetModeInfoCount(Point.Empty)))
@ -594,10 +590,18 @@ public class Av1EncoderFrameTests
Assert.False(mode.Skip); Assert.False(mode.Skip);
} }
using ImageFrame<Rgba32> decodedSecond = decoder.DecodeSequenceFrame<Rgba32>( using ImageFrame<Rgba32> decodedSecond = new(Configuration.Default, Width, Height);
decoder.DecodeSequenceFrame(
secondSample.ToArray(), secondSample.ToArray(),
null, null,
null); null,
decodedSecond.Size,
decodedSecond.Bounds,
decodedSecond.PixelBuffer.GetRegion(decodedSecond.Bounds),
default,
null,
null,
false);
ObuFrameHeader frameHeader = decoder.FrameHeader; ObuFrameHeader frameHeader = decoder.FrameHeader;
Assert.Equal(ObuFrameType.InterFrame, frameHeader.FrameType); Assert.Equal(ObuFrameType.InterFrame, frameHeader.FrameType);
@ -669,15 +673,31 @@ public class Av1EncoderFrameTests
encoder.EncodeInterFrame(second.Frames.RootFrame, secondSample); encoder.EncodeInterFrame(second.Frames.RootFrame, secondSample);
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using ImageFrame<Rgba32> decodedFirst = decoder.DecodeSequenceFrame<Rgba32>( using ImageFrame<Rgba32> decodedFirst = new(Configuration.Default, Width, Height);
decoder.DecodeSequenceFrame(
firstSample.ToArray(), firstSample.ToArray(),
null, null,
null); null,
decodedFirst.Size,
decodedFirst.Bounds,
decodedFirst.PixelBuffer.GetRegion(decodedFirst.Bounds),
default,
null,
null,
false);
using ImageFrame<Rgba32> decodedSecond = decoder.DecodeSequenceFrame<Rgba32>( using ImageFrame<Rgba32> decodedSecond = new(Configuration.Default, Width, Height);
decoder.DecodeSequenceFrame(
secondSample.ToArray(), secondSample.ToArray(),
null, null,
null); null,
decodedSecond.Size,
decodedSecond.Bounds,
decodedSecond.PixelBuffer.GetRegion(decodedSecond.Bounds),
default,
null,
null,
false);
ObuFrameHeader frameHeader = decoder.FrameHeader; ObuFrameHeader frameHeader = decoder.FrameHeader;
Av1GlobalMotionParameters globalMotion = frameHeader.GetGlobalMotionParameters()[0]; Av1GlobalMotionParameters globalMotion = frameHeader.GetGlobalMotionParameters()[0];
@ -767,7 +787,7 @@ public class Av1EncoderFrameTests
Assert.Equal(failureIndex, allocator.AllocationLog.Count); Assert.Equal(failureIndex, allocator.AllocationLog.Count);
Assert.All( Assert.All(
allocator.AllocationLog, allocator.AllocationLog,
allocation => Assert.Single(allocator.ReturnLog, returned => returned.AllocationId == allocation.AllocationId)); allocation => Assert.Single(allocator.ReturnLog, returned => returned.HashCodeOfBuffer == allocation.HashCodeOfBuffer));
Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count);
} }
@ -835,7 +855,7 @@ public class Av1EncoderFrameTests
Assert.Equal(expectedRowStorageLength, rowStorage.Length); Assert.Equal(expectedRowStorageLength, rowStorage.Length);
Assert.Contains( Assert.Contains(
allocator.ReturnLog, allocator.ReturnLog,
returned => returned.AllocationId == rowStorage.AllocationId); returned => returned.HashCodeOfBuffer == rowStorage.HashCodeOfBuffer);
} }
[Theory] [Theory]
@ -897,17 +917,6 @@ public class Av1EncoderFrameTests
effort); effort);
byte[] payload = stream.ToArray(); byte[] payload = stream.ToArray();
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
string outputName = $"encoder-frame-{width}x{height}-{bitDepth.GetBitCount()}b-444-lossless-effort{effort}";
File.WriteAllBytes(
Path.Combine(outputDirectory, outputName + ".obu"),
payload);
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using Av1FrameBuffer<byte> actual = decoder.DecodeFrameBuffer(payload, null, null, out _); using Av1FrameBuffer<byte> actual = decoder.DecodeFrameBuffer(payload, null, null, out _);
@ -919,9 +928,6 @@ public class Av1EncoderFrameTests
Assert.True(frameHeader.AllLossless); Assert.True(frameHeader.AllLossless);
Assert.Equal(Av1TransformMode.Only4x4, frameHeader.TransformMode); Assert.Equal(Av1TransformMode.Only4x4, frameHeader.TransformMode);
// Lossless native planes are the oracle for external decoding, not the packed RGB conversion on return.
// UInt16 raw samples are explicitly little-endian even when these tests run on a different host byte order.
using BinaryWriter rawOutput = new(File.Create(Path.Combine(outputDirectory, outputName + ".source.yuv")));
foreach (Av1Plane plane in new[] { Av1Plane.Y, Av1Plane.U, Av1Plane.V }) foreach (Av1Plane plane in new[] { Av1Plane.Y, Av1Plane.U, Av1Plane.V })
{ {
Buffer2DRegion<ushort> expectedPlane = expected.Frame.View.GetPlane(plane); Buffer2DRegion<ushort> expectedPlane = expected.Frame.View.GetPlane(plane);
@ -929,10 +935,6 @@ public class Av1EncoderFrameTests
{ {
ReadOnlySpan<ushort> expectedRow = expectedPlane.DangerousGetRowSpan(row); ReadOnlySpan<ushort> expectedRow = expectedPlane.DangerousGetRowSpan(row);
Assert.Equal(expectedRow, actual.GetHighBitDepthRowSpan(plane, row, 0, 0)); Assert.Equal(expectedRow, actual.GetHighBitDepthRowSpan(plane, row, 0, 0));
foreach (ushort sample in expectedRow)
{
rawOutput.Write(sample);
}
} }
} }
} }
@ -967,20 +969,15 @@ public class Av1EncoderFrameTests
using MemoryStream stream = new(); using MemoryStream stream = new();
Av1FrameEncoder.Encode(Configuration.Default, source.Frames.RootFrame, stream, colorConfig, qIndex: 0, effort); Av1FrameEncoder.Encode(Configuration.Default, source.Frames.RootFrame, stream, colorConfig, qIndex: 0, effort);
byte[] payload = stream.ToArray(); byte[] payload = stream.ToArray();
string outputDirectory = TestEnvironment.CreateOutputDirectory("Heif", "Av1", nameof(this.EncodeLosslessPartitionSearchAcrossClippedSuperblocks));
string outputName = $"{width}x{height}-effort{effort}";
File.WriteAllBytes(Path.Combine(outputDirectory, outputName + ".obu"), payload);
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using Av1FrameBuffer<byte> decoded = decoder.DecodeFrameBuffer(payload, null, null, out _); using Av1FrameBuffer<byte> decoded = decoder.DecodeFrameBuffer(payload, null, null, out _);
Assert.Equal(width, decoded.Width); Assert.Equal(width, decoded.Width);
Assert.Equal(height, decoded.Height); Assert.Equal(height, decoded.Height);
Buffer2DRegion<byte> actual = decoded.DeriveBlockPointer(Av1Plane.Y, 0, 0); Buffer2DRegion<byte> actual = decoded.DeriveBlockPointer(Av1Plane.Y, 0, 0);
using FileStream rawOutput = File.Create(Path.Combine(outputDirectory, outputName + ".source.yuv"));
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
ReadOnlySpan<byte> expectedRow = MemoryMarshal.AsBytes(source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y)); ReadOnlySpan<byte> expectedRow = MemoryMarshal.AsBytes(source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y));
Assert.Equal(expectedRow, actual.DangerousGetRowSpan(y)); Assert.Equal(expectedRow, actual.DangerousGetRowSpan(y));
rawOutput.Write(expectedRow);
} }
} }
@ -1027,7 +1024,23 @@ public class Av1EncoderFrameTests
byte[] payload = stream.ToArray(); byte[] payload = stream.ToArray();
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload); using Av1FrameBuffer<byte> decodedPlanes = decoder.DecodeFrameBuffer(payload, null, null, out _);
using Image<Rgba32> decoded = new(Configuration.Default, decodedPlanes.Width, decodedPlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
decodedPlanes,
decoded.Bounds,
decoded.Frames.RootFrame.PixelBuffer.GetRegion(decoded.Bounds),
decoded.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
decodedPlanes.ColorConfig.ColorRange);
Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo); Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
Point[] leafPositions = Point[] leafPositions =
[ [
@ -1090,7 +1103,23 @@ public class Av1EncoderFrameTests
byte[] payload = stream.ToArray(); byte[] payload = stream.ToArray();
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload); using Av1FrameBuffer<byte> decodedPlanes = decoder.DecodeFrameBuffer(payload, null, null, out _);
using Image<Rgba32> decoded = new(Configuration.Default, decodedPlanes.Width, decodedPlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
decodedPlanes,
decoded.Bounds,
decoded.Frames.RootFrame.PixelBuffer.GetRegion(decoded.Bounds),
decoded.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
decodedPlanes.ColorConfig.ColorRange);
Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo); Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
for (int modeInfoY = 4; modeInfoY < 8; modeInfoY++) for (int modeInfoY = 4; modeInfoY < 8; modeInfoY++)
{ {
@ -1130,7 +1159,23 @@ public class Av1EncoderFrameTests
byte[] payload = stream.ToArray(); byte[] payload = stream.ToArray();
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload); using Av1FrameBuffer<byte> decodedPlanes = decoder.DecodeFrameBuffer(payload, null, null, out _);
using Image<Rgba32> decoded = new(Configuration.Default, decodedPlanes.Width, decodedPlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
decodedPlanes,
decoded.Bounds,
decoded.Frames.RootFrame.PixelBuffer.GetRegion(decoded.Bounds),
decoded.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
decodedPlanes.ColorConfig.ColorRange);
Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo); Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
for (int modeInfoY = 0; modeInfoY < 8; modeInfoY++) for (int modeInfoY = 0; modeInfoY < 8; modeInfoY++)
{ {
@ -1173,7 +1218,23 @@ public class Av1EncoderFrameTests
byte[] payload = stream.ToArray(); byte[] payload = stream.ToArray();
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload); using Av1FrameBuffer<byte> decodedPlanes = decoder.DecodeFrameBuffer(payload, null, null, out _);
using Image<Rgba32> decoded = new(Configuration.Default, decodedPlanes.Width, decodedPlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
decodedPlanes,
decoded.Bounds,
decoded.Frames.RootFrame.PixelBuffer.GetRegion(decoded.Bounds),
decoded.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
decodedPlanes.ColorConfig.ColorRange);
Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo); Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
for (int modeInfoY = 0; modeInfoY < 16; modeInfoY++) for (int modeInfoY = 0; modeInfoY < 16; modeInfoY++)
{ {
@ -1217,7 +1278,23 @@ public class Av1EncoderFrameTests
Assert.True(sequenceHeader.Use128x128Superblock); Assert.True(sequenceHeader.Use128x128Superblock);
byte[] payload = stream.ToArray(); byte[] payload = stream.ToArray();
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload); using Av1FrameBuffer<byte> decodedPlanes = decoder.DecodeFrameBuffer(payload, null, null, out _);
using Image<Rgba32> decoded = new(Configuration.Default, decodedPlanes.Width, decodedPlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
decodedPlanes,
decoded.Bounds,
decoded.Frames.RootFrame.PixelBuffer.GetRegion(decoded.Bounds),
decoded.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
decodedPlanes.ColorConfig.ColorRange);
Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo); Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
for (int modeInfoY = 0; modeInfoY < 32; modeInfoY++) for (int modeInfoY = 0; modeInfoY < 32; modeInfoY++)
{ {
@ -1258,7 +1335,23 @@ public class Av1EncoderFrameTests
Assert.True(sequenceHeader.Use128x128Superblock); Assert.True(sequenceHeader.Use128x128Superblock);
byte[] payload = stream.ToArray(); byte[] payload = stream.ToArray();
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload); using Av1FrameBuffer<byte> decodedPlanes = decoder.DecodeFrameBuffer(payload, null, null, out _);
using Image<Rgba32> decoded = new(Configuration.Default, decodedPlanes.Width, decodedPlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
decodedPlanes,
decoded.Bounds,
decoded.Frames.RootFrame.PixelBuffer.GetRegion(decoded.Bounds),
decoded.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
decodedPlanes.ColorConfig.ColorRange);
Assert.Equal(new Size(Size, Size), decoded.Size); Assert.Equal(new Size(Size, Size), decoded.Size);
} }
@ -1293,7 +1386,22 @@ public class Av1EncoderFrameTests
byte[] payload = stream.ToArray(); byte[] payload = stream.ToArray();
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using Image<Rgba64> decoded = decoder.Decode<Rgba64>(payload); using Av1FrameBuffer<byte> decodedPlanes = decoder.DecodeFrameBuffer(payload, null, null, out _);
using Image<Rgba64> decoded = new(Configuration.Default, decodedPlanes.Width, decodedPlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
decodedPlanes,
decoded.Bounds,
decoded.Frames.RootFrame.PixelBuffer.GetRegion(decoded.Bounds),
decoded.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
decodedPlanes.ColorConfig.ColorRange);
Assert.True(encodedHeader.ColorConfig.IsMonochrome); Assert.True(encodedHeader.ColorConfig.IsMonochrome);
Assert.Equal( Assert.Equal(
@ -1305,17 +1413,6 @@ public class Av1EncoderFrameTests
Assert.Equal(decoded[0, 0].R, decoded[0, 0].G); Assert.Equal(decoded[0, 0].R, decoded[0, 0].G);
Assert.Equal(decoded[0, 0].R, decoded[0, 0].B); Assert.Equal(decoded[0, 0].R, decoded[0, 0].B);
Assert.Equal(ushort.MaxValue, decoded[0, 0].A); Assert.Equal(ushort.MaxValue, decoded[0, 0].A);
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(
Path.Combine(outputDirectory, $"encoder-alpha-{Width}x{Height}-{bitDepth.GetBitCount()}b.obu"),
payload);
} }
[Fact] [Fact]
@ -1363,7 +1460,7 @@ public class Av1EncoderFrameTests
Assert.Equal(typeof(float), allocation.ElementType); Assert.Equal(typeof(float), allocation.ElementType);
Assert.Equal(Width * 3, allocation.Length); Assert.Equal(Width * 3, allocation.Length);
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog); TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
Assert.Equal(allocation.AllocationId, returned.AllocationId); Assert.Equal(allocation.HashCodeOfBuffer, returned.HashCodeOfBuffer);
} }
[Theory] [Theory]
@ -1588,17 +1685,24 @@ public class Av1EncoderFrameTests
Assert.True(frameHeader.AllowScreenContentTools); Assert.True(frameHeader.AllowScreenContentTools);
Assert.True(frameHeader.AllowIntraBlockCopy); Assert.True(frameHeader.AllowIntraBlockCopy);
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload); using Av1FrameBuffer<byte> decodedPlanes = decoder.DecodeFrameBuffer(payload, null, null, out _);
Assert.Equal(new Size(width, height), decoded.Size); using Image<Rgba32> decoded = new(Configuration.Default, decodedPlanes.Width, decodedPlanes.Height);
Av1YuvConverter.ConvertToRgb(
string outputDirectory = Path.Combine( Configuration.Default,
TestEnvironment.ActualOutputDirectoryFullPath, decodedPlanes,
"Formats", decoded.Bounds,
"Heif", decoded.Frames.RootFrame.PixelBuffer.GetRegion(decoded.Bounds),
"Av1"); decoded.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
decodedPlanes.ColorConfig.ColorRange);
Directory.CreateDirectory(outputDirectory); Assert.Equal(new Size(width, height), decoded.Size);
File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-frame-16x16-8b-444-palette.obu"), payload);
} }
[Theory] [Theory]
@ -1644,7 +1748,23 @@ public class Av1EncoderFrameTests
byte[] payload = stream.ToArray(); byte[] payload = stream.ToArray();
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload); using Av1FrameBuffer<byte> decodedPlanes = decoder.DecodeFrameBuffer(payload, null, null, out _);
using Image<Rgba32> decoded = new(Configuration.Default, decodedPlanes.Width, decodedPlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
decodedPlanes,
decoded.Bounds,
decoded.Frames.RootFrame.PixelBuffer.GetRegion(decoded.Bounds),
decoded.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
decodedPlanes.ColorConfig.ColorRange);
ObuSequenceHeader sequenceHeader = Assert.IsType<ObuSequenceHeader>(decoder.SequenceHeader); ObuSequenceHeader sequenceHeader = Assert.IsType<ObuSequenceHeader>(decoder.SequenceHeader);
ObuFrameHeader frameHeader = Assert.IsType<ObuFrameHeader>(decoder.FrameHeader); ObuFrameHeader frameHeader = Assert.IsType<ObuFrameHeader>(decoder.FrameHeader);
Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo); Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
@ -1686,15 +1806,6 @@ public class Av1EncoderFrameTests
} }
Assert.NotEqual(0, modeCount); Assert.NotEqual(0, modeCount);
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(Path.Combine(outputDirectory, $"encoder-frame-16x16-8b-444-effort-{effort}.obu"), payload);
} }
[Fact] [Fact]
@ -1725,7 +1836,23 @@ public class Av1EncoderFrameTests
byte[] payload = stream.ToArray(); byte[] payload = stream.ToArray();
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using Image<L8> decoded = decoder.Decode<L8>(payload); using Av1FrameBuffer<byte> decodedPlanes = decoder.DecodeFrameBuffer(payload, null, null, out _);
using Image<L8> decoded = new(Configuration.Default, decodedPlanes.Width, decodedPlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
decodedPlanes,
decoded.Bounds,
decoded.Frames.RootFrame.PixelBuffer.GetRegion(decoded.Bounds),
decoded.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
decodedPlanes.ColorConfig.ColorRange);
Assert.NotNull(decoder.FrameHeader); Assert.NotNull(decoder.FrameHeader);
Assert.Equal(Av1TransformMode.Select, decoder.FrameHeader.TransformMode); Assert.Equal(Av1TransformMode.Select, decoder.FrameHeader.TransformMode);
Assert.NotNull(decoder.FrameInfo); Assert.NotNull(decoder.FrameInfo);
@ -1737,17 +1864,6 @@ public class Av1EncoderFrameTests
Assert.True(foundSplitTransform); Assert.True(foundSplitTransform);
Assert.Equal(new Size(Width, Height), decoded.Size); Assert.Equal(new Size(Width, Height), decoded.Size);
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(
Path.Combine(outputDirectory, "encoder-frame-16x16-8b-400-transform-size-select.obu"),
payload);
} }
[Theory] [Theory]
@ -1785,7 +1901,23 @@ public class Av1EncoderFrameTests
byte[] payload = stream.ToArray(); byte[] payload = stream.ToArray();
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload); using Av1FrameBuffer<byte> decodedPlanes = decoder.DecodeFrameBuffer(payload, null, null, out _);
using Image<Rgba32> decoded = new(Configuration.Default, decodedPlanes.Width, decodedPlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
decodedPlanes,
decoded.Bounds,
decoded.Frames.RootFrame.PixelBuffer.GetRegion(decoded.Bounds),
decoded.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
decodedPlanes.ColorConfig.ColorRange);
Assert.NotNull(decoder.FrameHeader); Assert.NotNull(decoder.FrameHeader);
Assert.True(decoder.FrameHeader.AllowScreenContentTools); Assert.True(decoder.FrameHeader.AllowScreenContentTools);
Assert.True(decoder.FrameHeader.AllowIntraBlockCopy); Assert.True(decoder.FrameHeader.AllowIntraBlockCopy);
@ -1802,19 +1934,6 @@ public class Av1EncoderFrameTests
Assert.True(usesIntraBlockCopy); Assert.True(usesIntraBlockCopy);
Assert.Equal(new Size(Width, Height), decoded.Size); Assert.Equal(new Size(Width, Height), decoded.Size);
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
string fileName = effort == 5
? "encoder-frame-328x16-8b-444-intrabc.obu"
: "encoder-frame-328x16-8b-444-intrabc-effort-6.obu";
File.WriteAllBytes(Path.Combine(outputDirectory, fileName), payload);
} }
[Fact] [Fact]
@ -1972,7 +2091,7 @@ public class Av1EncoderFrameTests
} }
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog); TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
Assert.Equal(allocation.AllocationId, returned.AllocationId); Assert.Equal(allocation.HashCodeOfBuffer, returned.HashCodeOfBuffer);
} }
[Fact] [Fact]
@ -2019,8 +2138,8 @@ public class Av1EncoderFrameTests
Assert.Equal(ExpectedTileOutputLength, tileOutput.Length); Assert.Equal(ExpectedTileOutputLength, tileOutput.Length);
Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count);
Assert.Equal( Assert.Equal(
allocator.AllocationLog.Select(allocation => allocation.AllocationId).Order(), allocator.AllocationLog.Select(allocation => allocation.HashCodeOfBuffer).Order(),
allocator.ReturnLog.Select(returned => returned.AllocationId).Order()); allocator.ReturnLog.Select(returned => returned.HashCodeOfBuffer).Order());
} }
private static ObuColorConfig CreateColorConfig( private static ObuColorConfig CreateColorConfig(

6
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderModeInfoBufferTests.cs

@ -45,7 +45,7 @@ public class Av1EncoderModeInfoBufferTests
} }
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog); TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
Assert.Equal(allocation.AllocationId, returned.AllocationId); Assert.Equal(allocation.HashCodeOfBuffer, returned.HashCodeOfBuffer);
} }
[Theory] [Theory]
@ -251,8 +251,8 @@ public class Av1EncoderModeInfoBufferTests
Assert.Equal(2, allocator.ReturnLog.Count); Assert.Equal(2, allocator.ReturnLog.Count);
Assert.Equal( Assert.Equal(
allocations.Select(x => x.AllocationId).Order(), allocations.Select(x => x.HashCodeOfBuffer).Order(),
allocator.ReturnLog.Select(x => x.AllocationId).Order()); allocator.ReturnLog.Select(x => x.HashCodeOfBuffer).Order());
} }
[Fact] [Fact]

14
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs

@ -1048,8 +1048,8 @@ public class Av1EntropyTests
} }
Assert.Equal( Assert.Equal(
allocator.AllocationLog.Select(x => x.AllocationId).Order(), allocator.AllocationLog.Select(x => x.HashCodeOfBuffer).Order(),
allocator.ReturnLog.Select(x => x.AllocationId).Order()); allocator.ReturnLog.Select(x => x.HashCodeOfBuffer).Order());
} }
[Theory] [Theory]
@ -1089,8 +1089,8 @@ public class Av1EntropyTests
} }
Assert.Equal( Assert.Equal(
Assert.Single(allocator.AllocationLog).AllocationId, Assert.Single(allocator.AllocationLog).HashCodeOfBuffer,
Assert.Single(allocator.ReturnLog).AllocationId); Assert.Single(allocator.ReturnLog).HashCodeOfBuffer);
} }
[Fact] [Fact]
@ -1149,7 +1149,7 @@ public class Av1EntropyTests
} }
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog); TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
Assert.Equal(allocation.AllocationId, returned.AllocationId); Assert.Equal(allocation.HashCodeOfBuffer, returned.HashCodeOfBuffer);
} }
/// <summary> /// <summary>
@ -1237,8 +1237,8 @@ public class Av1EntropyTests
Assert.Equal(3, allocator.ReturnLog.Count); Assert.Equal(3, allocator.ReturnLog.Count);
Assert.Equal( Assert.Equal(
allocator.AllocationLog.Select(x => x.AllocationId).Order(), allocator.AllocationLog.Select(x => x.HashCodeOfBuffer).Order(),
allocator.ReturnLog.Select(x => x.AllocationId).Order()); allocator.ReturnLog.Select(x => x.HashCodeOfBuffer).Order());
} }
[Fact] [Fact]

71
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1FrameBufferTests.cs

@ -6,9 +6,7 @@ using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter;
@ -103,18 +101,18 @@ public class Av1FrameBufferTests
{ {
int oldOwner = Assert.Single( int oldOwner = Assert.Single(
allocator.AllocationLog, allocator.AllocationLog,
x => x.ElementType == typeof(short) && x.Length == workspaceLengths[0]).AllocationId; x => x.ElementType == typeof(short) && x.Length == workspaceLengths[0]).HashCodeOfBuffer;
Assert.Single(allocator.ReturnLog, x => x.AllocationId == oldOwner); Assert.Single(allocator.ReturnLog, x => x.HashCodeOfBuffer == oldOwner);
} }
} }
using Av1FrameBuffer<byte> frame = decoder.DecodeFrameBuffer(payload, null, null, out _); using Av1FrameBuffer<byte> frame = decoder.DecodeFrameBuffer(payload, null, null, out _);
retainedLength = Math.Max(retainedLength, workspaceLengths[index]); retainedLength = Math.Max(retainedLength, workspaceLengths[index]);
int owner = Assert.Single( int owner = Assert.Single(
allocator.AllocationLog, x => x.ElementType == typeof(short) && x.Length == retainedLength).AllocationId; allocator.AllocationLog, x => x.ElementType == typeof(short) && x.Length == retainedLength).HashCodeOfBuffer;
Assert.DoesNotContain(allocator.ReturnLog, x => x.AllocationId == owner); Assert.DoesNotContain(allocator.ReturnLog, x => x.HashCodeOfBuffer == owner);
Buffer2DRegion<byte> luma = frame.DeriveBlockPointer(Av1Plane.Y, 0, 0); Buffer2DRegion<byte> luma = frame.DeriveBlockPointer(Av1Plane.Y, 0, 0);
for (int row = 0; row < frame.Height; row++) for (int row = 0; row < frame.Height; row++)
{ {
@ -124,7 +122,7 @@ public class Av1FrameBufferTests
} }
Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count);
Assert.All(allocator.AllocationLog, allocation => Assert.Single(allocator.ReturnLog, x => x.AllocationId == allocation.AllocationId)); Assert.All(allocator.AllocationLog, allocation => Assert.Single(allocator.ReturnLog, x => x.HashCodeOfBuffer == allocation.HashCodeOfBuffer));
} }
/// <summary> /// <summary>
@ -195,7 +193,7 @@ public class Av1FrameBufferTests
Assert.Equal(7, allocator.AllocationAttemptCount); Assert.Equal(7, allocator.AllocationAttemptCount);
Assert.Equal(6, allocator.AllocationLog.Count); Assert.Equal(6, allocator.AllocationLog.Count);
Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count);
Assert.All(allocator.AllocationLog, allocation => Assert.Single(allocator.ReturnLog, x => x.AllocationId == allocation.AllocationId)); Assert.All(allocator.AllocationLog, allocation => Assert.Single(allocator.ReturnLog, x => x.HashCodeOfBuffer == allocation.HashCodeOfBuffer));
} }
/// <summary> /// <summary>
@ -313,7 +311,7 @@ public class Av1FrameBufferTests
Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count);
foreach (TestMemoryAllocator.AllocationRequest allocation in allocator.AllocationLog) foreach (TestMemoryAllocator.AllocationRequest allocation in allocator.AllocationLog)
{ {
Assert.Single(allocator.ReturnLog, returned => returned.AllocationId == allocation.AllocationId); Assert.Single(allocator.ReturnLog, returned => returned.HashCodeOfBuffer == allocation.HashCodeOfBuffer);
} }
} }
@ -336,7 +334,7 @@ public class Av1FrameBufferTests
using (Av1FrameBuffer<byte> restored = Av1FrameBuffer<byte>.CreateRestoration(allocator, sequence, small)) using (Av1FrameBuffer<byte> restored = Av1FrameBuffer<byte>.CreateRestoration(allocator, sequence, small))
{ {
Assert.Throws<InvalidMemoryOperationException>(() => restored.ResizeRestoration(sequence, large)); Assert.Throws<InvalidMemoryOperationException>(() => restored.ResizeRestoration(sequence, large));
Assert.Equal(Assert.Single(allocator.AllocationLog).AllocationId, Assert.Single(allocator.ReturnLog).AllocationId); Assert.Equal(Assert.Single(allocator.AllocationLog).HashCodeOfBuffer, Assert.Single(allocator.ReturnLog).HashCodeOfBuffer);
restored.ResizeRestoration(sequence, large); restored.ResizeRestoration(sequence, large);
Assert.Equal(3, allocator.AllocationAttemptCount); Assert.Equal(3, allocator.AllocationAttemptCount);
@ -350,7 +348,7 @@ public class Av1FrameBufferTests
Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count);
foreach (TestMemoryAllocator.AllocationRequest allocation in allocator.AllocationLog) foreach (TestMemoryAllocator.AllocationRequest allocation in allocator.AllocationLog)
{ {
Assert.Single(allocator.ReturnLog, returned => returned.AllocationId == allocation.AllocationId); Assert.Single(allocator.ReturnLog, returned => returned.HashCodeOfBuffer == allocation.HashCodeOfBuffer);
} }
} }
@ -497,7 +495,7 @@ public class Av1FrameBufferTests
Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count);
foreach (TestMemoryAllocator.AllocationRequest allocation in allocator.AllocationLog) foreach (TestMemoryAllocator.AllocationRequest allocation in allocator.AllocationLog)
{ {
Assert.Single(allocator.ReturnLog, returned => returned.AllocationId == allocation.AllocationId); Assert.Single(allocator.ReturnLog, returned => returned.HashCodeOfBuffer == allocation.HashCodeOfBuffer);
} }
} }
@ -547,7 +545,7 @@ public class Av1FrameBufferTests
Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count);
foreach (TestMemoryAllocator.AllocationRequest allocation in allocator.AllocationLog) foreach (TestMemoryAllocator.AllocationRequest allocation in allocator.AllocationLog)
{ {
Assert.Single(allocator.ReturnLog, returned => returned.AllocationId == allocation.AllocationId); Assert.Single(allocator.ReturnLog, returned => returned.HashCodeOfBuffer == allocation.HashCodeOfBuffer);
} }
} }
@ -646,7 +644,7 @@ public class Av1FrameBufferTests
} }
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog); TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
Assert.Equal(allocation.AllocationId, returned.AllocationId); Assert.Equal(allocation.HashCodeOfBuffer, returned.HashCodeOfBuffer);
} }
/// <summary> /// <summary>
@ -747,7 +745,7 @@ public class Av1FrameBufferTests
} }
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog); TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
Assert.Equal(allocation.AllocationId, returned.AllocationId); Assert.Equal(allocation.HashCodeOfBuffer, returned.HashCodeOfBuffer);
} }
/// <summary> /// <summary>
@ -793,9 +791,6 @@ public class Av1FrameBufferTests
ModeInfoRowCount = 16 ModeInfoRowCount = 16
}; };
using Av1LoopFilterContext loopFilterContext =
new(Configuration.Default.MemoryAllocator, sequenceHeader, frameHeader);
using Av1ReferenceFrameStore referenceFrames = new(); using Av1ReferenceFrameStore referenceFrames = new();
// Reset frame-plane logs so these assertions describe the supplied workspace and the borrowing decoder. // Reset frame-plane logs so these assertions describe the supplied workspace and the borrowing decoder.
@ -826,7 +821,6 @@ public class Av1FrameBufferTests
sequenceHeader, sequenceHeader,
frameHeader, frameHeader,
frameBuffer, frameBuffer,
loopFilterContext,
referenceFrames, referenceFrames,
workspace.Memory); workspace.Memory);
@ -837,44 +831,7 @@ public class Av1FrameBufferTests
} }
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog); TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
Assert.Equal(workspaceAllocation.AllocationId, returned.AllocationId); Assert.Equal(workspaceAllocation.HashCodeOfBuffer, returned.HashCodeOfBuffer);
}
/// <summary>
/// Verifies that failure to allocate the active chroma transform map releases the preceding luma map.
/// </summary>
[Fact]
public void LoopFilterContextAllocationFailureReleasesLumaMap()
{
FailingTestMemoryAllocator allocator = new(failureAllocationNumber: 2);
ObuSequenceHeader sequenceHeader = new()
{
MaxFrameWidth = 64,
MaxFrameHeight = 64,
Use128x128Superblock = false,
ColorConfig = new ObuColorConfig
{
IsMonochrome = false,
SubSamplingX = true,
SubSamplingY = true,
BitDepth = Av1BitDepth.EightBit
}
};
ObuFrameHeader frameHeader = new()
{
ModeInfoColumnCount = 16,
ModeInfoRowCount = 16
};
Assert.Throws<InvalidMemoryOperationException>(
() => new Av1LoopFilterContext(allocator, sequenceHeader, frameHeader));
TestMemoryAllocator.AllocationRequest allocation = Assert.Single(allocator.AllocationLog);
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
Assert.Equal(2, allocator.AllocationAttemptCount);
Assert.Equal(allocation.HashCodeOfBuffer, returned.HashCodeOfBuffer);
} }
/// <summary> /// <summary>

145
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs

@ -6,6 +6,7 @@ using System.Numerics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif; using SixLabors.ImageSharp.Formats.Heif;
using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Color;
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
@ -165,24 +166,36 @@ public class Av1IntraSuperblockEncoderTests
// Sequence decoding retains the first frame's reference slots. The still-image transfer API deliberately // Sequence decoding retains the first frame's reference slots. The still-image transfer API deliberately
// releases those slots, so it cannot be used between dependent samples. Full-range monochrome L8 is exact. // releases those slots, so it cannot be used between dependent samples. Full-range monochrome L8 is exact.
using ImageFrame<L8> decodedFirst = decoder.DecodeSequenceFrame<L8>(firstSample.ToArray(), null, null); using ImageFrame<L8> decodedFirst = new(configuration, Width, Height);
using ImageFrame<L8> decodedSecond = decoder.DecodeSequenceFrame<L8>(secondSample.ToArray(), null, null); decoder.DecodeSequenceFrame(
firstSample.ToArray(),
// Preserve both the production stream and every managed reconstructed luma sample for exact libaom comparison. null,
string outputDirectory = TestEnvironment.CreateOutputDirectory("Heif", "Av1", nameof(this.ProductionTileSelectsNonRegularInterpolation)); null,
string outputName = $"{filter}-{dualFilter}"; decodedFirst.Size,
using FileStream output = File.Create(Path.Combine(outputDirectory, outputName + ".obu")); decodedFirst.Bounds,
firstSample.Position = 0; decodedFirst.PixelBuffer.GetRegion(decodedFirst.Bounds),
firstSample.CopyTo(output); default,
secondSample.Position = 0; null,
secondSample.CopyTo(output); null,
using FileStream rawOutput = File.Create(Path.Combine(outputDirectory, outputName + ".managed.yuv")); false);
using ImageFrame<L8> decodedSecond = new(configuration, Width, Height);
decoder.DecodeSequenceFrame(
secondSample.ToArray(),
null,
null,
decodedSecond.Size,
decodedSecond.Bounds,
decodedSecond.PixelBuffer.GetRegion(decodedSecond.Bounds),
default,
null,
null,
false);
for (int y = 0; y < Height; y++) for (int y = 0; y < Height; y++)
{ {
ReadOnlySpan<byte> expected = reference.Frame.View.GetPlane(Av1Plane.Y).DangerousGetRowSpan(y); ReadOnlySpan<byte> expected = reference.Frame.View.GetPlane(Av1Plane.Y).DangerousGetRowSpan(y);
ReadOnlySpan<byte> actual = MemoryMarshal.AsBytes(decodedFirst.PixelBuffer.DangerousGetRowSpan(y)); ReadOnlySpan<byte> actual = MemoryMarshal.AsBytes(decodedFirst.PixelBuffer.DangerousGetRowSpan(y));
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
rawOutput.Write(actual);
} }
for (int y = 0; y < Height; y++) for (int y = 0; y < Height; y++)
@ -190,7 +203,6 @@ public class Av1IntraSuperblockEncoderTests
ReadOnlySpan<byte> expected = reconstruction.Frame.View.GetPlane(Av1Plane.Y).DangerousGetRowSpan(y); ReadOnlySpan<byte> expected = reconstruction.Frame.View.GetPlane(Av1Plane.Y).DangerousGetRowSpan(y);
ReadOnlySpan<byte> actual = MemoryMarshal.AsBytes(decodedSecond.PixelBuffer.DangerousGetRowSpan(y)); ReadOnlySpan<byte> actual = MemoryMarshal.AsBytes(decodedSecond.PixelBuffer.DangerousGetRowSpan(y));
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
rawOutput.Write(actual);
} }
} }
@ -330,19 +342,9 @@ public class Av1IntraSuperblockEncoderTests
using MemoryStream secondSample = new(); using MemoryStream secondSample = new();
using ObuWriter obuWriter = new(configuration); using ObuWriter obuWriter = new(configuration);
obuWriter.WriteFrame(secondSample, sequenceHeader, frameHeader, tileWriter); obuWriter.WriteFrame(secondSample, sequenceHeader, frameHeader, tileWriter);
string outputDirectory = TestEnvironment.CreateOutputDirectory("Heif", "Av1", nameof(this.ProductionTileSelectsDualAxisInterpolationHighBitDepth));
string outputName = $"{bitDepth}-{horizontalFilter}-{verticalFilter}";
using FileStream output = File.Create(Path.Combine(outputDirectory, outputName + ".obu"));
firstSample.Position = 0;
firstSample.CopyTo(output);
secondSample.Position = 0;
secondSample.CopyTo(output);
using BinaryWriter rawOutput = new(File.Create(Path.Combine(outputDirectory, outputName + ".managed.yuv")));
using Av1Decoder decoder = new(configuration); using Av1Decoder decoder = new(configuration);
for (int frameIndex = 0; frameIndex < 2; frameIndex++) for (int frameIndex = 0; frameIndex < 2; frameIndex++)
{ {
// Consume native retained planes before the next sample can replace them. BinaryWriter emits explicit
// little-endian UInt16 samples, matching the raw reference-decoder output independently of host byte order.
decoder.DecodeSequenceReference((frameIndex == 0 ? firstSample : secondSample).ToArray(), null, null); decoder.DecodeSequenceReference((frameIndex == 0 ? firstSample : secondSample).ToArray(), null, null);
Av1FrameBuffer<byte> decoded = Assert.IsType<Av1FrameBuffer<byte>>(decoder.FrameBuffer); Av1FrameBuffer<byte> decoded = Assert.IsType<Av1FrameBuffer<byte>>(decoder.FrameBuffer);
Buffer2DRegion<ushort> expected = (frameIndex == 0 ? reference : reconstruction).Frame.View.GetPlane(Av1Plane.Y); Buffer2DRegion<ushort> expected = (frameIndex == 0 ? reference : reconstruction).Frame.View.GetPlane(Av1Plane.Y);
@ -350,10 +352,6 @@ public class Av1IntraSuperblockEncoderTests
{ {
ReadOnlySpan<ushort> actualRow = decoded.GetHighBitDepthRowSpan(Av1Plane.Y, y, 0, 0); ReadOnlySpan<ushort> actualRow = decoded.GetHighBitDepthRowSpan(Av1Plane.Y, y, 0, 0);
Assert.Equal(expected.DangerousGetRowSpan(y), actualRow); Assert.Equal(expected.DangerousGetRowSpan(y), actualRow);
foreach (ushort sample in actualRow)
{
rawOutput.Write(sample);
}
} }
} }
} }
@ -1945,20 +1943,6 @@ public class Av1IntraSuperblockEncoderTests
} }
} }
string outputDirectory = Path.Combine(TestEnvironment.ActualOutputDirectoryFullPath, "Formats", "Heif", "Av1");
string outputName = $"encoder-palette-chroma-{width}x{height}-{subX}-{subY}-{useLumaPalette}";
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(Path.Combine(outputDirectory, outputName + ".obu"), payload);
using FileStream raw = File.Create(Path.Combine(outputDirectory, outputName + ".retained.yuv"));
for (int plane = 0; plane < 3; plane++)
{
Buffer2DRegion<byte> retained = reconstruction.Frame.View.GetPlane((Av1Plane)plane);
for (int row = 0; row < retained.Height; row++)
{
raw.Write(retained.DangerousGetRowSpan(row));
}
}
Assert.NotEqual(0, tileWriter.GetTileData(0).Length); Assert.NotEqual(0, tileWriter.GetTileData(0).Length);
} }
@ -3009,24 +2993,29 @@ public class Av1IntraSuperblockEncoderTests
byte[] payload = WriteCompleteTileObu(pictureTemplate, tileWriter, Width, Height); byte[] payload = WriteCompleteTileObu(pictureTemplate, tileWriter, Width, Height);
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload); using Av1FrameBuffer<byte> decodedPlanes = decoder.DecodeFrameBuffer(payload, null, null, out _);
using Image<Rgba32> decoded = new(Configuration.Default, decodedPlanes.Width, decodedPlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
decodedPlanes,
decoded.Bounds,
decoded.Frames.RootFrame.PixelBuffer.GetRegion(decoded.Bounds),
decoded.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
decodedPlanes.ColorConfig.ColorRange);
Assert.NotNull(decoder.FrameInfo); Assert.NotNull(decoder.FrameInfo);
Av1BlockModeInfo decodedBlock = decoder.FrameInfo.GetModeInfoAt(new Point(2, 2)); Av1BlockModeInfo decodedBlock = decoder.FrameInfo.GetModeInfoAt(new Point(2, 2));
Assert.True(decodedBlock.UseFilterIntra); Assert.True(decodedBlock.UseFilterIntra);
Assert.Equal(filterIntraMode, decodedBlock.FilterIntraMode); Assert.Equal(filterIntraMode, decodedBlock.FilterIntraMode);
Assert.Equal(4, decodedBlock.GetTransformUnitCount(Av1Plane.Y)); Assert.Equal(4, decodedBlock.GetTransformUnitCount(Av1Plane.Y));
Assert.Equal(new Size(Width, Height), decoded.Size); Assert.Equal(new Size(Width, Height), decoded.Size);
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(
Path.Combine(outputDirectory, $"encoder-filter-intra-transform-size-select-{bitDepth}b.obu"),
payload);
} }
Assert.NotEqual(0, pilotWriter.GetTileData(0).Length); Assert.NotEqual(0, pilotWriter.GetTileData(0).Length);
@ -3665,16 +3654,6 @@ public class Av1IntraSuperblockEncoderTests
} }
Assert.True(hasMixedPartition); Assert.True(hasMixedPartition);
string directory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath, "Heif", "Av1", nameof(this.ProductionMixedPartitionsPreserveReconstructionOrder));
Directory.CreateDirectory(directory);
File.WriteAllBytes(Path.Combine(directory, $"{size}-{transpose}-{enableIntraEdgeFilter}.obu"), payload);
using FileStream raw = File.Create(Path.Combine(directory, $"{size}-{transpose}-{enableIntraEdgeFilter}.retained.yuv"));
for (int y = 0; y < size; y++)
{
raw.Write(retainedPlane.DangerousGetRowSpan(y));
}
} }
[Theory] [Theory]
@ -3785,11 +3764,6 @@ public class Av1IntraSuperblockEncoderTests
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using Av1FrameBuffer<byte> decodedFrame = decoder.DecodeFrameBuffer(payload, null, null, out _); using Av1FrameBuffer<byte> decodedFrame = decoder.DecodeFrameBuffer(payload, null, null, out _);
int changedSamples = 0; int changedSamples = 0;
string directory = Path.Combine(TestEnvironment.ActualOutputDirectoryFullPath, "Heif", "Av1", "ProductionDeblocking");
Directory.CreateDirectory(directory);
string name = $"{bitDepth}-{colorFormat}";
File.WriteAllBytes(Path.Combine(directory, name + ".obu"), payload);
using FileStream raw = File.Create(Path.Combine(directory, name + ".retained.yuv"));
for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) for (int planeIndex = 0; planeIndex < planeCount; planeIndex++)
{ {
Av1Plane plane = (Av1Plane)planeIndex; Av1Plane plane = (Av1Plane)planeIndex;
@ -3806,8 +3780,6 @@ public class Av1IntraSuperblockEncoderTests
{ {
changedSamples += row[x] != unfiltered[planeIndex][(y * retained.Width) + x] ? 1 : 0; changedSamples += row[x] != unfiltered[planeIndex][(y * retained.Width) + x] ? 1 : 0;
} }
raw.Write(MemoryMarshal.AsBytes(row));
} }
} }
@ -4128,7 +4100,23 @@ public class Av1IntraSuperblockEncoderTests
// Decode every payload, including clipped maps: retained reconstruction alone cannot reveal missing map symbols. // Decode every payload, including clipped maps: retained reconstruction alone cannot reveal missing map symbols.
byte[] payload = WriteCompleteTileObu(pictureTemplate, tileWriter, width, height); byte[] payload = WriteCompleteTileObu(pictureTemplate, tileWriter, width, height);
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using Image<Rgba32> decoded = decoder.Decode<Rgba32>(payload); using Av1FrameBuffer<byte> decodedPlanes = decoder.DecodeFrameBuffer(payload, null, null, out _);
using Image<Rgba32> decoded = new(Configuration.Default, decodedPlanes.Width, decodedPlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
decodedPlanes,
decoded.Bounds,
decoded.Frames.RootFrame.PixelBuffer.GetRegion(decoded.Bounds),
decoded.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
decodedPlanes.ColorConfig.ColorRange);
Assert.NotNull(decoder.FrameInfo); Assert.NotNull(decoder.FrameInfo);
Av1BlockModeInfo decodedBlock = decoder.FrameInfo.GetModeInfoAt(default); Av1BlockModeInfo decodedBlock = decoder.FrameInfo.GetModeInfoAt(default);
Assert.True(decodedBlock.GetPaletteSize(Av1Plane.Y) > 0); Assert.True(decodedBlock.GetPaletteSize(Av1Plane.Y) > 0);
@ -4148,19 +4136,6 @@ public class Av1IntraSuperblockEncoderTests
Assert.Equal(4, decodedBlock.GetTransformUnitCount(Av1Plane.Y)); Assert.Equal(4, decodedBlock.GetTransformUnitCount(Av1Plane.Y));
} }
string outputDirectory = Path.Combine(TestEnvironment.ActualOutputDirectoryFullPath, "Formats", "Heif", "Av1");
string outputName = useSplitTransform
? $"encoder-palette-transform-size-select-{bitDepthValue}b"
: $"encoder-palette-luma-{bitDepthValue}b-{width}x{height}";
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(Path.Combine(outputDirectory, outputName + ".obu"), payload);
using FileStream raw = File.Create(Path.Combine(outputDirectory, outputName + ".retained.yuv"));
for (int row = 0; row < height; row++)
{
raw.Write(MemoryMarshal.AsBytes(reconstructionPlane.DangerousGetRowSpan(row)[..width]));
}
Assert.NotEqual(0, tileWriter.GetTileData(0).Length); Assert.NotEqual(0, tileWriter.GetTileData(0).Length);
} }

79
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionSearchTests.cs

@ -52,8 +52,6 @@ public class Av1MotionSearchTests
const int QIndex = 90; const int QIndex = 90;
const int ReferenceStride = 192; const int ReferenceStride = 192;
const int ReferenceOrigin = (64 * ReferenceStride) + 64; const int ReferenceOrigin = (64 * ReferenceStride) + 64;
string directory = Path.Combine(TestEnvironment.ActualOutputDirectoryFullPath, "Heif", "Av1", "SingleReferenceSearch");
Directory.CreateDirectory(directory);
using Av1EncoderBlockWorkspace workspace = new(Configuration.Default, allocateInterMotionCosts: true); using Av1EncoderBlockWorkspace workspace = new(Configuration.Default, allocateInterMotionCosts: true);
using Av1SymbolEncoder writer = new(Configuration.Default, 64, QIndex, updateCdf: true); using Av1SymbolEncoder writer = new(Configuration.Default, 64, QIndex, updateCdf: true);
Av1MotionVectorCosts costs = workspace.GetMotionVectorCosts(Av1MotionVectorPrecision.EighthSample); Av1MotionVectorCosts costs = workspace.GetMotionVectorCosts(Av1MotionVectorPrecision.EighthSample);
@ -142,24 +140,6 @@ public class Av1MotionSearchTests
costs); costs);
Av1MotionSearchBase.SingleReferenceState state = default; Av1MotionSearchBase.SingleReferenceState state = default;
using FileStream stream = File.Create(Path.Combine(directory, $"{bits}-{width}-{pattern}-{speed}-{forceInteger}.bin"));
using BinaryWriter output = new(stream);
int[] header =
[
3, bits, width, height, sourceStride, ReferenceStride, ReferenceOrigin,
bounds.Left, bounds.Top, bounds.Right, bounds.Bottom, QIndex, speed,
(int)settings.GetFullPixelMethod(blockSize), frameStep, spatialMagnitude, 1, searchRange,
forceInteger ? 1 : 0, 1, 0, multiplier, Av1RateDistortion.GetMotionSearchSadPerBit(QIndex, bitDepth),
settings.AutomaticStepSizeLevel, settings.StartCandidatePruningLevel, settings.ReferenceCandidatePruningLevel,
(int)settings.FractionalMethod, settings.FractionalIterationsPerStep, settings.FractionalInterpolationTaps,
(int)settings.SecondCandidateSelection, settings.MeshErrorThreshold, settings.MeshPruningLevel,
settings.DownsampledSadLevel, frameSize.Width, frameSize.Height, 0, 0, 512, 1024, 256, 0, 0, 3,
source.Length, reference.Length, pattern
];
output.Write(MemoryMarshal.AsBytes(header.AsSpan()));
output.Write(MemoryMarshal.AsBytes(source.AsSpan()));
output.Write(MemoryMarshal.AsBytes(reference.AsSpan()));
for (int referenceIndex = 0; referenceIndex < 3; referenceIndex++) for (int referenceIndex = 0; referenceIndex < 3; referenceIndex++)
{ {
Av1MotionVector referenceVector = referenceIndex == 1 Av1MotionVector referenceVector = referenceIndex == 1
@ -218,17 +198,6 @@ public class Av1MotionSearchTests
Assert.Equal(0, result.Vector.Row & 7); Assert.Equal(0, result.Vector.Row & 7);
Assert.Equal(0, result.Vector.Column & 7); Assert.Equal(0, result.Vector.Column & 7);
} }
int[] decision =
[
referenceVector.Row, referenceVector.Column, drlRate, valid ? 1 : 0,
valid ? result.Vector.Row : 0, valid ? result.Vector.Column : 0,
valid ? retained.Rate : 0, retained.Skip ? 1 : 0, state.StartCount,
retained.HasFullResult ? 1 : 0, retained.FullVector.Row, retained.FullVector.Column,
retained.FullRate, retained.FullCost
];
output.Write(MemoryMarshal.AsBytes(decision.AsSpan()));
} }
Assert.Equal(originalSource, source); Assert.Equal(originalSource, source);
@ -241,7 +210,7 @@ public class Av1MotionSearchTests
} }
/// <summary> /// <summary>
/// Checks all search methods at each sample precision and exports their inputs for independent reference verification. /// Checks all search methods at each sample precision.
/// </summary> /// </summary>
/// <param name="bits">The coded component precision.</param> /// <param name="bits">The coded component precision.</param>
[Theory] [Theory]
@ -307,8 +276,6 @@ public class Av1MotionSearchTests
const int ReferenceStride = 192; const int ReferenceStride = 192;
const int ReferenceOrigin = (64 * ReferenceStride) + 64; const int ReferenceOrigin = (64 * ReferenceStride) + 64;
int maximum = (1 << bits) - 1; int maximum = (1 << bits) - 1;
string directory = Path.Combine(TestEnvironment.ActualOutputDirectoryFullPath, "Heif", "Av1", "FullPixelSearch");
Directory.CreateDirectory(directory);
using Av1EncoderBlockWorkspace workspace = new(Configuration.Default, allocateInterMotionCosts: true); using Av1EncoderBlockWorkspace workspace = new(Configuration.Default, allocateInterMotionCosts: true);
using Av1SymbolEncoder writer = new(Configuration.Default, 64, QIndex, updateCdf: true); using Av1SymbolEncoder writer = new(Configuration.Default, 64, QIndex, updateCdf: true);
Av1MotionVectorCosts costs = workspace.GetMotionVectorCosts(Av1MotionVectorPrecision.EighthSample); Av1MotionVectorCosts costs = workspace.GetMotionVectorCosts(Av1MotionVectorPrecision.EighthSample);
@ -423,34 +390,10 @@ public class Av1MotionSearchTests
Assert.Equal(0, result.SquaredError); Assert.Equal(0, result.SquaredError);
} }
// This export records both source inputs and the published state. A native comparison can
// establish controller agreement without calling native code from the managed test process.
using BinaryWriter output = new(File.Create(Path.Combine(directory, $"{bits}-{width}-{pattern}-{methodIndex}.bin")));
foreach (int value in new[]
{
1, bits, width, height, sourceStride, ReferenceStride, ReferenceOrigin,
frameBounds.Left, frameBounds.Top, frameBounds.Right, frameBounds.Bottom,
referenceVector.Row, referenceVector.Column, start.X, start.Y, 5, methodIndex, (int)speed,
frameSize.Width, frameSize.Height, QIndex, 0, screenContent ? 1 : 0, 0, 0, source.Length, reference.Length,
result.Vector.X, result.Vector.Y, result.Variance, result.SquaredError, result.MotionCost,
secondBest.HasValue ? 1 : 0, secondBest.GetValueOrDefault().X, secondBest.GetValueOrDefault().Y, multiplier, sadPerBit
})
{
output.Write(value);
}
foreach (int cost in costList)
{
output.Write(cost);
}
output.Write(MemoryMarshal.AsBytes(source.AsSpan()));
output.Write(MemoryMarshal.AsBytes(reference.AsSpan()));
if (method == FullPixelSearchMethod.NStep) if (method == FullPixelSearchMethod.NStep)
{ {
VerifyFractionalSearches<TSample, TOperator>( VerifyFractionalSearches<TSample, TOperator>(
bitDepth, bitDepth,
bits,
pattern, pattern,
width, width,
source, source,
@ -464,9 +407,7 @@ public class Av1MotionSearchTests
multiplier, multiplier,
result, result,
costList, costList,
writer, writer);
directory,
methodIndex);
} }
} }
} }
@ -478,7 +419,6 @@ public class Av1MotionSearchTests
/// </summary> /// </summary>
private static void VerifyFractionalSearches<TSample, TOperator>( private static void VerifyFractionalSearches<TSample, TOperator>(
Av1BitDepth bitDepth, Av1BitDepth bitDepth,
int bits,
int pattern, int pattern,
int width, int width,
TSample[] source, TSample[] source,
@ -492,9 +432,7 @@ public class Av1MotionSearchTests
int multiplier, int multiplier,
Av1MotionSearchBase.FullPixelResult integerResult, Av1MotionSearchBase.FullPixelResult integerResult,
int[] costList, int[] costList,
Av1SymbolEncoder writer, Av1SymbolEncoder writer)
string directory,
int fullPixelMethod)
where TSample : unmanaged where TSample : unmanaged
where TOperator : struct, Av1MotionSearchBase.IMotionSearchOperator<TSample> where TOperator : struct, Av1MotionSearchBase.IMotionSearchOperator<TSample>
{ {
@ -516,7 +454,6 @@ public class Av1MotionSearchTests
bitDepth, bitDepth,
multiplier); multiplier);
using BinaryWriter output = new(File.Create(Path.Combine(directory, $"{bits}-{width}-{pattern}-{fullPixelMethod}.fractional")));
foreach (FractionalSearchMethod method in Enum.GetValues<FractionalSearchMethod>()) foreach (FractionalSearchMethod method in Enum.GetValues<FractionalSearchMethod>())
{ {
foreach (int taps in new[] { 2, 4, 8 }) foreach (int taps in new[] { 2, 4, 8 })
@ -562,16 +499,6 @@ public class Av1MotionSearchTests
out Av1MotionSearchBase.FractionalResult repeated); out Av1MotionSearchBase.FractionalResult repeated);
Assert.Equal(precision == SearchPrecision.Integer ? cost : int.MaxValue, repeatedCost); Assert.Equal(precision == SearchPrecision.Integer ? cost : int.MaxValue, repeatedCost);
foreach (int value in new[]
{
(int)method, taps, variant, allowHighPrecision ? 1 : 0, iterations, retainStatistics ? 1 : 0, retainCosts ? 1 : 0,
cost, result.Vector.Column, result.Vector.Row, result.Variance, result.SquaredError, result.MotionCost,
centers[0].Row, centers[0].Column, centers[1].Row, centers[1].Column, centers[2].Row, centers[2].Column,
repeatedCost, repeated.Vector.Column, repeated.Vector.Row, repeated.Variance, repeated.SquaredError, repeated.MotionCost
})
{
output.Write(value);
}
} }
} }
} }

99
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs

@ -7,7 +7,6 @@ using System.Text;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Heif; using SixLabors.ImageSharp.Formats.Heif;
using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -591,15 +590,14 @@ public class Av1ReconstructionConformanceTests
TestImageProvider<Rgba32> provider, TestImageProvider<Rgba32> provider,
HeifBitDepth bitDepth) HeifBitDepth bitDepth)
{ {
using Image<Rgba32> image = provider.GetImage(); using Image<Rgba32> image = provider.GetImage(HeifDecoder.Instance);
HeifMetadata metadata = image.Metadata.GetHeifMetadata(); HeifMetadata metadata = image.Metadata.GetHeifMetadata();
Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod);
Assert.Equal(bitDepth, metadata.BitDepth); Assert.Equal(bitDepth, metadata.BitDepth);
CicpProfile colorProfile = Assert.IsType<CicpProfile>(image.Metadata.CicpProfile); CicpProfile colorProfile = Assert.IsType<CicpProfile>(image.Metadata.CicpProfile);
Assert.Equal(CicpColorPrimaries.ItuRBt709_6, colorProfile.ColorPrimaries); Assert.Equal(CicpColorPrimaries.ItuRBt709_6, colorProfile.ColorPrimaries);
Assert.Equal(CicpTransferCharacteristics.Iec61966_2_1, colorProfile.TransferCharacteristics); Assert.Equal(CicpTransferCharacteristics.Iec61966_2_1, colorProfile.TransferCharacteristics);
Assert.Equal(CicpMatrixCoefficients.ItuRBt601_7_525, colorProfile.MatrixCoefficients); Assert.Equal(CicpMatrixCoefficients.Identity, colorProfile.MatrixCoefficients);
Assert.True(colorProfile.FullRange); Assert.True(colorProfile.FullRange);
FeatureTestRunner.RunWithHwIntrinsicsFeature( FeatureTestRunner.RunWithHwIntrinsicsFeature(
@ -1035,21 +1033,26 @@ public class Av1ReconstructionConformanceTests
continue; continue;
} }
ImageFrame<Rgba32> decodedFrame; using ImageFrame<Rgba32> frame = new(configuration, track.CodedWidth, track.CodedHeight);
try try
{ {
decodedFrame = decoder.DecodeSequenceFrame<Rgba32>( decoder.DecodeSequenceFrame(
sampleData, sampleData,
track.CicpProfile, track.CicpProfile,
track.Av1CodecConfiguration); track.Av1CodecConfiguration,
frame.Size,
frame.Bounds,
frame.PixelBuffer.GetRegion(frame.Bounds),
default,
null,
null,
false);
} }
catch (InvalidImageContentException exception) catch (InvalidImageContentException exception)
{ {
throw new InvalidImageContentException($"The verified compound fixture failed at sample {sampleIndex}.", exception); throw new InvalidImageContentException($"The verified compound fixture failed at sample {sampleIndex}.", exception);
} }
using ImageFrame<Rgba32> frame = decodedFrame;
_ = Assert.IsType<ObuFrameHeader>(decoder.FrameHeader); _ = Assert.IsType<ObuFrameHeader>(decoder.FrameHeader);
Av1FrameBuffer<byte> frameBuffer = Assert.IsType<Av1FrameBuffer<byte>>(decoder.FrameBuffer); Av1FrameBuffer<byte> frameBuffer = Assert.IsType<Av1FrameBuffer<byte>>(decoder.FrameBuffer);
Av1FrameInfo frameInfo = decoder.FrameInfo; Av1FrameInfo frameInfo = decoder.FrameInfo;
@ -1290,10 +1293,18 @@ public class Av1ReconstructionConformanceTests
{ {
int payloadLength = checked((int)BinaryPrimitives.ReadUInt32LittleEndian(ivf.AsSpan(ivfOffset, 4))); int payloadLength = checked((int)BinaryPrimitives.ReadUInt32LittleEndian(ivf.AsSpan(ivfOffset, 4)));
ivfOffset += 12; ivfOffset += 12;
using ImageFrame<Rgba32> frame = decoder.DecodeSequenceFrame<Rgba32>( using ImageFrame<Rgba32> frame = new(Configuration.Default, OfficialMotionVectorFixtureWidth, OfficialMotionVectorFixtureHeight);
decoder.DecodeSequenceFrame(
ivf.AsSpan(ivfOffset, payloadLength), ivf.AsSpan(ivfOffset, payloadLength),
null, null,
null); null,
frame.Size,
frame.Bounds,
frame.PixelBuffer.GetRegion(frame.Bounds),
default,
null,
null,
false);
ivfOffset += payloadLength; ivfOffset += payloadLength;
Assert.Equal(OfficialMotionVectorFixtureWidth, frame.Width); Assert.Equal(OfficialMotionVectorFixtureWidth, frame.Width);
@ -1408,10 +1419,18 @@ public class Av1ReconstructionConformanceTests
{ {
int payloadLength = checked((int)BinaryPrimitives.ReadUInt32LittleEndian(ivf.AsSpan(ivfOffset, 4))); int payloadLength = checked((int)BinaryPrimitives.ReadUInt32LittleEndian(ivf.AsSpan(ivfOffset, 4)));
ivfOffset += 12; ivfOffset += 12;
using ImageFrame<Rgba32> frame = decoder.DecodeSequenceFrame<Rgba32>( using ImageFrame<Rgba32> frame = new(Configuration.Default, OfficialIntraBlockCopyFixtureWidth, OfficialIntraBlockCopyFixtureHeight);
decoder.DecodeSequenceFrame(
ivf.AsSpan(ivfOffset, payloadLength), ivf.AsSpan(ivfOffset, payloadLength),
null, null,
null); null,
frame.Size,
frame.Bounds,
frame.PixelBuffer.GetRegion(frame.Bounds),
default,
null,
null,
false);
ivfOffset += payloadLength; ivfOffset += payloadLength;
Assert.Equal(OfficialIntraBlockCopyFixtureWidth, frame.Width); Assert.Equal(OfficialIntraBlockCopyFixtureWidth, frame.Width);
@ -2128,10 +2147,18 @@ public class Av1ReconstructionConformanceTests
{ {
int payloadLength = checked((int)BinaryPrimitives.ReadUInt32LittleEndian(ivf.AsSpan(ivfOffset, 4))); int payloadLength = checked((int)BinaryPrimitives.ReadUInt32LittleEndian(ivf.AsSpan(ivfOffset, 4)));
ivfOffset += 12; ivfOffset += 12;
using ImageFrame<Rgba32> frame = decoder.DecodeSequenceFrame<Rgba32>( using ImageFrame<Rgba32> frame = new(configuration, expectedWidth, expectedHeight);
decoder.DecodeSequenceFrame(
ivf.AsSpan(ivfOffset, payloadLength), ivf.AsSpan(ivfOffset, payloadLength),
null, null,
null); null,
frame.Size,
frame.Bounds,
frame.PixelBuffer.GetRegion(frame.Bounds),
default,
null,
null,
false);
ivfOffset += payloadLength; ivfOffset += payloadLength;
Assert.Equal(expectedWidth, frame.Width); Assert.Equal(expectedWidth, frame.Width);
@ -2255,10 +2282,18 @@ public class Av1ReconstructionConformanceTests
{ {
int payloadLength = checked((int)BinaryPrimitives.ReadUInt32LittleEndian(ivf.AsSpan(ivfOffset, 4))); int payloadLength = checked((int)BinaryPrimitives.ReadUInt32LittleEndian(ivf.AsSpan(ivfOffset, 4)));
ivfOffset += 12; ivfOffset += 12;
ImageFrame<Rgba32> decodedFrame = decoder.DecodeSequenceFrame<Rgba32>( ImageFrame<Rgba32> decodedFrame = new(configuration, OfficialMotionVectorFixtureWidth, OfficialMotionVectorFixtureHeight);
decoder.DecodeSequenceFrame(
ivf.AsSpan(ivfOffset, payloadLength), ivf.AsSpan(ivfOffset, payloadLength),
null, null,
null); null,
decodedFrame.Size,
decodedFrame.Bounds,
decodedFrame.PixelBuffer.GetRegion(decodedFrame.Bounds),
default,
null,
null,
false);
using ImageFrame<Rgba32> frame = decodedFrame; using ImageFrame<Rgba32> frame = decodedFrame;
@ -2489,10 +2524,18 @@ public class Av1ReconstructionConformanceTests
continue; continue;
} }
using ImageFrame<Rgba32> frame = decoder.DecodeSequenceFrame<Rgba32>( using ImageFrame<Rgba32> frame = new(configuration, track.CodedWidth, track.CodedHeight);
decoder.DecodeSequenceFrame(
sampleData, sampleData,
track.CicpProfile, track.CicpProfile,
track.Av1CodecConfiguration); track.Av1CodecConfiguration,
frame.Size,
frame.Bounds,
frame.PixelBuffer.GetRegion(frame.Bounds),
default,
null,
null,
false);
Av1FrameBuffer<byte> frameBuffer = Assert.IsType<Av1FrameBuffer<byte>>(decoder.FrameBuffer); Av1FrameBuffer<byte> frameBuffer = Assert.IsType<Av1FrameBuffer<byte>>(decoder.FrameBuffer);
coverage |= GetInterPredictionCoverage(decoder); coverage |= GetInterPredictionCoverage(decoder);
@ -3836,7 +3879,6 @@ public class Av1ReconstructionConformanceTests
Assert.Equal(height, image.Height); Assert.Equal(height, image.Height);
Assert.Single(image.Frames); Assert.Single(image.Frames);
HeifMetadata metadata = image.Metadata.GetHeifMetadata(); HeifMetadata metadata = image.Metadata.GetHeifMetadata();
Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod);
Assert.Equal(metadataBitDepth, metadata.BitDepth); Assert.Equal(metadataBitDepth, metadata.BitDepth);
if (metadataBitDepth != HeifBitDepth.Bit8) if (metadataBitDepth != HeifBitDepth.Bit8)
@ -3877,13 +3919,12 @@ public class Av1ReconstructionConformanceTests
int height, int height,
HeifBitDepth bitDepth) HeifBitDepth bitDepth)
{ {
using Image<Rgba32> image = provider.GetImage(); using Image<Rgba32> image = provider.GetImage(HeifDecoder.Instance);
Assert.Equal(width, image.Width); Assert.Equal(width, image.Width);
Assert.Equal(height, image.Height); Assert.Equal(height, image.Height);
Assert.Single(image.Frames); Assert.Single(image.Frames);
HeifMetadata metadata = image.Metadata.GetHeifMetadata(); HeifMetadata metadata = image.Metadata.GetHeifMetadata();
Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod);
Assert.Equal(bitDepth, metadata.BitDepth); Assert.Equal(bitDepth, metadata.BitDepth);
} }
@ -3896,12 +3937,9 @@ public class Av1ReconstructionConformanceTests
TestImageProvider<Rgba32> provider = TestImageProvider<Rgba32> provider =
FeatureTestRunner.DeserializeForXunit<TestImageProvider<Rgba32>>(providerDump); FeatureTestRunner.DeserializeForXunit<TestImageProvider<Rgba32>>(providerDump);
using Image<Rgba32> image = provider.GetImage(); using Image<Rgba32> image = provider.GetImage(HeifDecoder.Instance);
// CICP records the AVIF source component layout, but PNG permits only the identity matrix. The debug image
// is a pixel artifact; the test verifies source metadata independently where that is part of the contract.
image.DebugSave(provider, new PngEncoder { SkipMetadata = true });
image.DebugSave(provider, extension: "png", encoder: new PngEncoder());
image.CompareToReferenceOutput(ImageComparer.Exact, provider); image.CompareToReferenceOutput(ImageComparer.Exact, provider);
} }
@ -3914,13 +3952,10 @@ public class Av1ReconstructionConformanceTests
TestImageProvider<Rgba32> provider = TestImageProvider<Rgba32> provider =
FeatureTestRunner.DeserializeForXunit<TestImageProvider<Rgba32>>(providerDump); FeatureTestRunner.DeserializeForXunit<TestImageProvider<Rgba32>>(providerDump);
using Image<Rgba32> sequence = provider.GetImage(); using Image<Rgba32> sequence = provider.GetImage(HeifDecoder.Instance);
using Image<Rgba32> finalFrame = sequence.Frames.CloneFrame(sequence.Frames.Count - 1); using Image<Rgba32> finalFrame = sequence.Frames.CloneFrame(sequence.Frames.Count - 1);
// The retained source CICP matrix cannot be represented in a PNG cICP chunk. Omit metadata only from the finalFrame.DebugSave(provider, extension: "png", encoder: new PngEncoder());
// diagnostic output; the exact reference comparison below still consumes the original decoded image.
finalFrame.DebugSave(provider, new PngEncoder { SkipMetadata = true });
finalFrame.CompareToReferenceOutput(ImageComparer.Exact, provider); finalFrame.CompareToReferenceOutput(ImageComparer.Exact, provider);
} }

8
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceFrameStoreTests.cs

@ -265,10 +265,10 @@ public class Av1ReferenceFrameStoreTests
// Full current-frame state owns the projected temporal field. The retained frame owns only the source field // Full current-frame state owns the projected temporal field. The retained frame owns only the source field
// needed by later projections, irrespective of how many map and output aliases identify the same frame. // needed by later projections, irrespective of how many map and output aliases identify the same frame.
frameInfo.Dispose(); frameInfo.Dispose();
Assert.Single(allocator.ReturnLog, returned => returned.AllocationId == temporalMotionField.AllocationId); Assert.Single(allocator.ReturnLog, returned => returned.HashCodeOfBuffer == temporalMotionField.HashCodeOfBuffer);
Av1ReferenceFrame output = store.TakeOutput(); Av1ReferenceFrame output = store.TakeOutput();
Assert.DoesNotContain(allocator.ReturnLog, returned => returned.AllocationId == retainedMotionField.AllocationId); Assert.DoesNotContain(allocator.ReturnLog, returned => returned.HashCodeOfBuffer == retainedMotionField.HashCodeOfBuffer);
output.Dispose(); output.Dispose();
output.Dispose(); output.Dispose();
@ -278,7 +278,7 @@ public class Av1ReferenceFrameStoreTests
allocator.AllocationLog, allocator.AllocationLog,
allocation => Assert.Single( allocation => Assert.Single(
allocator.ReturnLog, allocator.ReturnLog,
returned => returned.AllocationId == allocation.AllocationId)); returned => returned.HashCodeOfBuffer == allocation.HashCodeOfBuffer));
Assert.Equal(2, allocator.ReturnLog.Count); Assert.Equal(2, allocator.ReturnLog.Count);
} }
@ -343,7 +343,7 @@ public class Av1ReferenceFrameStoreTests
allocator.AllocationLog, allocator.AllocationLog,
allocation => Assert.Single( allocation => Assert.Single(
allocator.ReturnLog, allocator.ReturnLog,
returned => returned.AllocationId == allocation.AllocationId)); returned => returned.HashCodeOfBuffer == allocation.HashCodeOfBuffer));
Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count);
} }

18
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1RegularQuantizerTests.cs

@ -30,13 +30,6 @@ public class Av1RegularQuantizerTests
/// </summary> /// </summary>
private static void ValidateQuantization() private static void ValidateQuantization()
{ {
string vectorWidth = Vector512.IsHardwareAccelerated ? "512"
: Vector256.IsHardwareAccelerated ? "256"
: Vector128.IsHardwareAccelerated ? "128" : "0";
string directory = Path.Combine(TestEnvironment.ActualOutputDirectoryFullPath, "Heif", "Av1", "RegularQuantization", vectorWidth);
Directory.CreateDirectory(directory);
foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.EightBit, Av1BitDepth.TenBit, Av1BitDepth.TwelveBit }) foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.EightBit, Av1BitDepth.TenBit, Av1BitDepth.TwelveBit })
{ {
foreach (Av1TransformSize size in new[] foreach (Av1TransformSize size in new[]
@ -128,17 +121,6 @@ public class Av1RegularQuantizerTests
} }
Assert.Equal(expectedEnd, end); Assert.Equal(expectedEnd, end);
using BinaryWriter output = new(File.Create(Path.Combine(
directory, $"{bits}-{(int)size}-{qIndex}-{sharpness}-{pattern}.bin")));
foreach (int value in new[] { bits, (int)size, qIndex, dcDelta, acDelta, sharpness, count, end })
{
output.Write(value);
}
output.Write(MemoryMarshal.AsBytes(input.AsSpan(1, count)));
output.Write(MemoryMarshal.AsBytes(quantized.AsSpan(1, count)));
output.Write(MemoryMarshal.AsBytes(dequantized.AsSpan(1, count)));
} }
} }
} }

43
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TilingTests.cs

@ -2,7 +2,9 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers; using System.Buffers;
using SixLabors.ImageSharp.Formats.Heif;
using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Color;
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
@ -137,7 +139,7 @@ public class Av1TilingTests
Assert.Equal(failureIndex, allocator.AllocationLog.Count); Assert.Equal(failureIndex, allocator.AllocationLog.Count);
Assert.All( Assert.All(
allocator.AllocationLog, allocator.AllocationLog,
allocation => Assert.Single(allocator.ReturnLog, returned => returned.AllocationId == allocation.AllocationId)); allocation => Assert.Single(allocator.ReturnLog, returned => returned.HashCodeOfBuffer == allocation.HashCodeOfBuffer));
Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count);
} }
@ -173,7 +175,23 @@ public class Av1TilingTests
const int codedItemOffset = 0x17A8; const int codedItemOffset = 0x17A8;
const int codedItemLength = 0x3AE4; const int codedItemLength = 0x3AE4;
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
using Image<Rgba32> image = decoder.Decode<Rgba32>(content.AsSpan(codedItemOffset, codedItemLength)); using Av1FrameBuffer<byte> imagePlanes = decoder.DecodeFrameBuffer(content.AsSpan(codedItemOffset, codedItemLength), null, null, out _);
using Image<Rgba32> image = new(Configuration.Default, imagePlanes.Width, imagePlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
imagePlanes,
image.Bounds,
image.Frames.RootFrame.PixelBuffer.GetRegion(image.Bounds),
image.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
imagePlanes.ColorConfig.ColorRange);
Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo); Av1FrameInfo frameInfo = Assert.IsType<Av1FrameInfo>(decoder.FrameInfo);
ObuFrameHeader frameHeader = Assert.IsType<ObuFrameHeader>(decoder.FrameHeader); ObuFrameHeader frameHeader = Assert.IsType<ObuFrameHeader>(decoder.FrameHeader);
Span<int> blockSizeCounts = stackalloc int[(int)Av1BlockSize.AllSizes]; Span<int> blockSizeCounts = stackalloc int[(int)Av1BlockSize.AllSizes];
@ -213,7 +231,22 @@ public class Av1TilingTests
byte[] content = File.ReadAllBytes(filePath); byte[] content = File.ReadAllBytes(filePath);
Av1Decoder decoder = new(Configuration.Default); Av1Decoder decoder = new(Configuration.Default);
using Image<Rgba32> image = decoder.Decode<Rgba32>(content.AsSpan(0x010E, 0x001D)); using Av1FrameBuffer<byte> imagePlanes = decoder.DecodeFrameBuffer(content.AsSpan(0x010E, 0x001D), null, null, out _);
using Image<Rgba32> image = new(Configuration.Default, imagePlanes.Width, imagePlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
imagePlanes,
image.Bounds,
image.Frames.RootFrame.PixelBuffer.GetRegion(image.Bounds),
image.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
imagePlanes.ColorConfig.ColorRange);
Assert.Equal(4, image.Width); Assert.Equal(4, image.Width);
Assert.Equal(4, image.Height); Assert.Equal(4, image.Height);
@ -306,7 +339,7 @@ public class Av1TilingTests
using IMemoryOwner<short> workspace = using IMemoryOwner<short> workspace =
frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(obuReader.SequenceHeader)); frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(obuReader.SequenceHeader));
using Av1FrameDecoder frameDecoder = new( Av1FrameDecoder frameDecoder = new(
obuReader.SequenceHeader, obuReader.SequenceHeader,
obuReader.FrameHeader, obuReader.FrameHeader,
frameInfo, frameInfo,
@ -361,7 +394,7 @@ public class Av1TilingTests
using IMemoryOwner<short> workspace = using IMemoryOwner<short> workspace =
frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(obuReader.SequenceHeader)); frameBuffer.MemoryAllocator.Allocate<short>(Av1BlockDecoder.GetWorkspaceLength(obuReader.SequenceHeader));
using Av1FrameDecoder frameDecoder = new( Av1FrameDecoder frameDecoder = new(
obuReader.SequenceHeader, obuReader.SequenceHeader,
obuReader.FrameHeader, obuReader.FrameHeader,
frameInfo, frameInfo,

2
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs

@ -653,7 +653,7 @@ public class Av1TransformBlockEncoderTests
} }
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog); TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
Assert.Equal(allocation.AllocationId, returned.AllocationId); Assert.Equal(allocation.HashCodeOfBuffer, returned.HashCodeOfBuffer);
} }
private static void ValidateBlock( private static void ValidateBlock(

28
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformEstimateTests.cs

@ -31,13 +31,6 @@ public class Av1TransformEstimateTests
/// </summary> /// </summary>
private static void ValidateEstimates() private static void ValidateEstimates()
{ {
string vectorWidth = Vector512.IsHardwareAccelerated ? "512"
: Vector256.IsHardwareAccelerated ? "256"
: Vector128.IsHardwareAccelerated ? "128" : "0";
string directory = Path.Combine(TestEnvironment.ActualOutputDirectoryFullPath, "Heif", "Av1", "TransformEstimates", vectorWidth);
Directory.CreateDirectory(directory);
using Av1EncoderBlockWorkspace workspace = new(Configuration.Default); using Av1EncoderBlockWorkspace workspace = new(Configuration.Default);
foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.EightBit, Av1BitDepth.TenBit, Av1BitDepth.TwelveBit }) foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.EightBit, Av1BitDepth.TenBit, Av1BitDepth.TwelveBit })
{ {
@ -220,27 +213,6 @@ public class Av1TransformEstimateTests
Assert.Equal(long.MaxValue, terminated); Assert.Equal(long.MaxValue, terminated);
Assert.Equal(Av1RateDistortionStatistics.Invalid, incomplete); Assert.Equal(Av1RateDistortionStatistics.Invalid, incomplete);
} }
using BinaryWriter output = new(File.Create(Path.Combine(
directory, $"{bits}-{width}-{height}-{(int)transformSize}-{qIndex}-{pattern}.bin")));
foreach (int value in new[]
{
bits, width, height, (int)transformSize, qIndex, sharpness, lossless ? 1 : 0,
multiplier, partitionRate, noSkipRate, skipRate, stride
})
{
output.Write(value);
}
output.Write(cost);
output.Write((long)statistics.Rate);
output.Write(statistics.Distortion);
output.Write(energy);
output.Write(skip ? 1L : 0L);
output.Write(above);
output.Write(left);
output.Write(MemoryMarshal.AsBytes(residual.AsSpan()));
} }
} }
} }

285
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs

@ -3,6 +3,7 @@
using System; using System;
using System.Numerics; using System.Numerics;
using SixLabors.ImageSharp.Formats.Heif;
using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Color; using SixLabors.ImageSharp.Formats.Heif.Av1.Color;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
@ -116,7 +117,20 @@ public class Av1YuvConverterTests
frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0)[0] = (byte)v; frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0)[0] = (byte)v;
// Act // Act
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, frame); Av1YuvConverter.ConvertToRgb(
Configuration.Default,
frameBuffer,
new Rectangle(Point.Empty, frame.Size),
frame.PixelBuffer.GetRegion(),
frame.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
frameBuffer.ColorConfig.ColorRange);
// Assert // Assert
frame.DangerousTryGetSinglePixelMemory(out Memory<Rgb24> memory); frame.DangerousTryGetSinglePixelMemory(out Memory<Rgb24> memory);
@ -126,6 +140,60 @@ public class Av1YuvConverterTests
Assert.Equal(b, actual.B, 1d); Assert.Equal(b, actual.B, 1d);
} }
[Fact]
public void YuvToRgbWritesOnlyRequestedDestinationRegion()
{
Rgb24 sentinel = new(201, 202, 203);
using Image<Rgb24> image = new(7, 5, sentinel);
ObuSequenceHeader header = CreateSequenceHeader(4, 3, true, ObuMatrixCoefficients.Identity);
using Av1FrameBuffer<byte> buffer = new(Configuration.Default, header, Av1ColorFormat.Yuv444, false);
for (int y = 0; y < 3; y++)
{
Span<byte> green = buffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(y);
Span<byte> blue = buffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(y);
Span<byte> red = buffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(y);
for (int x = 0; x < 4; x++)
{
red[x] = (byte)(10 + x);
green[x] = (byte)(20 + y);
blue[x] = (byte)(30 + x + (y * 4));
}
}
Rectangle source = new(1, 1, 2, 2);
Rectangle target = new(3, 2, 2, 2);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
buffer,
source,
image.Frames.RootFrame.PixelBuffer.GetRegion(target),
new Size(4, 3),
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
buffer.ColorConfig.ColorRange);
// The identity matrix gives independently known RGB values. Every pixel outside the target must retain
// the sentinel, including the row prefix and suffix surrounding the converted source crop.
for (int y = 0; y < image.Height; y++)
{
for (int x = 0; x < image.Width; x++)
{
int sourceX = x - target.X + source.X;
int sourceY = y - target.Y + source.Y;
Rgb24 expected = target.Contains(x, y)
? new Rgb24((byte)(10 + sourceX), (byte)(20 + sourceY), (byte)(30 + sourceX + (sourceY * 4)))
: sentinel;
Assert.Equal(expected, image[x, y]);
}
}
}
/// <summary> /// <summary>
/// Verifies that limited-range monochrome samples expand to the complete RGB output range. /// Verifies that limited-range monochrome samples expand to the complete RGB output range.
/// </summary> /// </summary>
@ -147,7 +215,20 @@ public class Av1YuvConverterTests
yRow[1] = 235; yRow[1] = 235;
// Act // Act
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, image.Frames.RootFrame); Av1YuvConverter.ConvertToRgb(
Configuration.Default,
frameBuffer,
new Rectangle(Point.Empty, image.Frames.RootFrame.Size),
image.Frames.RootFrame.PixelBuffer.GetRegion(),
image.Frames.RootFrame.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
frameBuffer.ColorConfig.ColorRange);
// Assert // Assert
Span<Rgb24> actual = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); Span<Rgb24> actual = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0);
@ -220,7 +301,20 @@ public class Av1YuvConverterTests
frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, 0, 0, 0).Fill(neutralChroma); frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, 0, 0, 0).Fill(neutralChroma);
// Act // Act
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, image.Frames.RootFrame); Av1YuvConverter.ConvertToRgb(
Configuration.Default,
frameBuffer,
new Rectangle(Point.Empty, image.Frames.RootFrame.Size),
image.Frames.RootFrame.PixelBuffer.GetRegion(),
image.Frames.RootFrame.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
frameBuffer.ColorConfig.ColorRange);
// Assert // Assert
Span<Rgb24> actual = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); Span<Rgb24> actual = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0);
@ -372,7 +466,20 @@ public class Av1YuvConverterTests
frameBuffer.DeriveBlockPointer(Av1Plane.V, 1, 0).DangerousGetRowSpan(0).Fill(128); frameBuffer.DeriveBlockPointer(Av1Plane.V, 1, 0).DangerousGetRowSpan(0).Fill(128);
// Act // Act
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, image.Frames.RootFrame); Av1YuvConverter.ConvertToRgb(
Configuration.Default,
frameBuffer,
new Rectangle(Point.Empty, image.Frames.RootFrame.Size),
image.Frames.RootFrame.PixelBuffer.GetRegion(),
image.Frames.RootFrame.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Bilinear,
frameBuffer.ColorConfig.ColorRange);
// Assert // Assert
Span<Rgb24> actual = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); Span<Rgb24> actual = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0);
@ -419,7 +526,20 @@ public class Av1YuvConverterTests
vPlane.DangerousGetRowSpan(1).Fill(128); vPlane.DangerousGetRowSpan(1).Fill(128);
// Act // Act
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, image.Frames.RootFrame); Av1YuvConverter.ConvertToRgb(
Configuration.Default,
frameBuffer,
new Rectangle(Point.Empty, image.Frames.RootFrame.Size),
image.Frames.RootFrame.PixelBuffer.GetRegion(),
image.Frames.RootFrame.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Bilinear,
frameBuffer.ColorConfig.ColorRange);
// Assert // Assert
Assert.Equal(expectedTopBlue, image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[1].B); Assert.Equal(expectedTopBlue, image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[1].B);
@ -538,7 +658,20 @@ public class Av1YuvConverterTests
CreateTestData(rnd, frameBuffer, Av1Plane.V); CreateTestData(rnd, frameBuffer, Av1Plane.V);
// Act // Act
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, frame); Av1YuvConverter.ConvertToRgb(
Configuration.Default,
frameBuffer,
new Rectangle(Point.Empty, frame.Size),
frame.PixelBuffer.GetRegion(),
frame.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
frameBuffer.ColorConfig.ColorRange);
Span<Rgb24> referenceOutput = Av1ReferenceYuvConverter.YuvToRgb(frameBuffer, true); Span<Rgb24> referenceOutput = Av1ReferenceYuvConverter.YuvToRgb(frameBuffer, true);
// Assert // Assert
@ -643,7 +776,20 @@ public class Av1YuvConverterTests
// Act // Act
Av1YuvConverter.ConvertFromRgb(Configuration.Default, frame, frameBuffer); Av1YuvConverter.ConvertFromRgb(Configuration.Default, frame, frameBuffer);
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, actual.Frames.RootFrame); Av1YuvConverter.ConvertToRgb(
Configuration.Default,
frameBuffer,
new Rectangle(Point.Empty, actual.Frames.RootFrame.Size),
actual.Frames.RootFrame.PixelBuffer.GetRegion(),
actual.Frames.RootFrame.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
frameBuffer.ColorConfig.ColorRange);
// Assert // Assert
actual.Frames.RootFrame.DangerousTryGetSinglePixelMemory(out Memory<Rgb24> actualMemory); actual.Frames.RootFrame.DangerousTryGetSinglePixelMemory(out Memory<Rgb24> actualMemory);
@ -690,7 +836,20 @@ public class Av1YuvConverterTests
// Act // Act
Av1YuvConverter.ConvertFromRgb(Configuration.Default, image.Frames.RootFrame, frameBuffer); Av1YuvConverter.ConvertFromRgb(Configuration.Default, image.Frames.RootFrame, frameBuffer);
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, actual.Frames.RootFrame); Av1YuvConverter.ConvertToRgb(
Configuration.Default,
frameBuffer,
new Rectangle(Point.Empty, actual.Frames.RootFrame.Size),
actual.Frames.RootFrame.PixelBuffer.GetRegion(),
actual.Frames.RootFrame.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
frameBuffer.ColorConfig.ColorRange);
// Assert // Assert
Span<Rgb24> actualPixels = actual.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); Span<Rgb24> actualPixels = actual.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0);
@ -729,7 +888,20 @@ public class Av1YuvConverterTests
Assert.Equal(198, frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0)[0]); Assert.Equal(198, frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0)[0]);
using Image<Rgb24> destination = new(1, 1); using Image<Rgb24> destination = new(1, 1);
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame); Av1YuvConverter.ConvertToRgb(
Configuration.Default,
frameBuffer,
new Rectangle(Point.Empty, destination.Frames.RootFrame.Size),
destination.Frames.RootFrame.PixelBuffer.GetRegion(),
destination.Frames.RootFrame.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
frameBuffer.ColorConfig.ColorRange);
Assert.Equal(new Rgb24(150, 100, 51), destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0]); Assert.Equal(new Rgb24(150, 100, 51), destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0]);
} }
@ -794,7 +966,20 @@ public class Av1YuvConverterTests
Assert.Equal(expectedV, actualV); Assert.Equal(expectedV, actualV);
using Image<Rgb48> destination = new(1, 1); using Image<Rgb48> destination = new(1, 1);
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame); Av1YuvConverter.ConvertToRgb(
Configuration.Default,
frameBuffer,
new Rectangle(Point.Empty, destination.Frames.RootFrame.Size),
destination.Frames.RootFrame.PixelBuffer.GetRegion(),
destination.Frames.RootFrame.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
frameBuffer.ColorConfig.ColorRange);
Assert.Equal(new Rgb48(ushort.MaxValue, 0, 0), destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0]); Assert.Equal(new Rgb48(ushort.MaxValue, 0, 0), destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0]);
} }
@ -865,7 +1050,20 @@ public class Av1YuvConverterTests
} }
using Image<Rgb48> destination = new(2, 1); using Image<Rgb48> destination = new(2, 1);
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame); Av1YuvConverter.ConvertToRgb(
Configuration.Default,
frameBuffer,
new Rectangle(Point.Empty, destination.Frames.RootFrame.Size),
destination.Frames.RootFrame.PixelBuffer.GetRegion(),
destination.Frames.RootFrame.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
frameBuffer.ColorConfig.ColorRange);
Span<Rgb48> destinationPixels = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); Span<Rgb48> destinationPixels = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0);
Assert.Equal(new Rgb48(0, 0, 0), destinationPixels[0]); Assert.Equal(new Rgb48(0, 0, 0), destinationPixels[0]);
@ -896,7 +1094,20 @@ public class Av1YuvConverterTests
() => Av1YuvConverter.ConvertFromRgb(Configuration.Default, image.Frames.RootFrame, frameBuffer)); () => Av1YuvConverter.ConvertFromRgb(Configuration.Default, image.Frames.RootFrame, frameBuffer));
Assert.Throws<InvalidImageContentException>( Assert.Throws<InvalidImageContentException>(
() => Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, image.Frames.RootFrame)); () => Av1YuvConverter.ConvertToRgb(
Configuration.Default,
frameBuffer,
new Rectangle(Point.Empty, image.Frames.RootFrame.Size),
image.Frames.RootFrame.PixelBuffer.GetRegion(),
image.Frames.RootFrame.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
frameBuffer.ColorConfig.ColorRange));
} }
/// <summary> /// <summary>
@ -937,7 +1148,20 @@ public class Av1YuvConverterTests
using Image<Rgb48> destination = new(width, 1); using Image<Rgb48> destination = new(width, 1);
Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer); Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer);
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame); Av1YuvConverter.ConvertToRgb(
Configuration.Default,
frameBuffer,
new Rectangle(Point.Empty, destination.Frames.RootFrame.Size),
destination.Frames.RootFrame.PixelBuffer.GetRegion(),
destination.Frames.RootFrame.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
frameBuffer.ColorConfig.ColorRange);
AssertPlaneContainsRepeatedSample(frameBuffer, Av1Plane.Y, 0, 0); AssertPlaneContainsRepeatedSample(frameBuffer, Av1Plane.Y, 0, 0);
AssertPlaneContainsRepeatedSample(frameBuffer, Av1Plane.U, 0, 0); AssertPlaneContainsRepeatedSample(frameBuffer, Av1Plane.U, 0, 0);
@ -1017,7 +1241,20 @@ public class Av1YuvConverterTests
// Act // Act
Av1YuvConverter.ConvertFromRgb(Configuration.Default, frame, frameBuffer); Av1YuvConverter.ConvertFromRgb(Configuration.Default, frame, frameBuffer);
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, actual.Frames.RootFrame); Av1YuvConverter.ConvertToRgb(
Configuration.Default,
frameBuffer,
new Rectangle(Point.Empty, actual.Frames.RootFrame.Size),
actual.Frames.RootFrame.PixelBuffer.GetRegion(),
actual.Frames.RootFrame.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
frameBuffer.ColorConfig.ColorRange);
// Assert // Assert
ImageComparer.Tolerant(0.002F).VerifySimilarity(image, actual); ImageComparer.Tolerant(0.002F).VerifySimilarity(image, actual);
@ -1088,10 +1325,11 @@ public class Av1YuvConverterTests
Av1YuvConverter.ComposeAlpha( Av1YuvConverter.ComposeAlpha(
Configuration.Default, Configuration.Default,
frameBuffer, frameBuffer,
destination.Frames.RootFrame, destination.Frames.RootFrame.PixelBuffer.GetRegion(destination.Frames.RootFrame.Bounds),
destination.Size, destination.Size,
destination.Bounds, destination.Bounds,
false); false,
default);
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
@ -1144,10 +1382,11 @@ public class Av1YuvConverterTests
Av1YuvConverter.ComposeAlpha( Av1YuvConverter.ComposeAlpha(
Configuration.Default, Configuration.Default,
frameBuffer, frameBuffer,
destination.Frames.RootFrame, destination.Frames.RootFrame.PixelBuffer.GetRegion(destination.Frames.RootFrame.Bounds),
destination.Size, destination.Size,
destination.Bounds, destination.Bounds,
false); false,
default);
for (int y = 0; y < destinationHeight; y++) for (int y = 0; y < destinationHeight; y++)
{ {
@ -1200,10 +1439,11 @@ public class Av1YuvConverterTests
Av1YuvConverter.ComposeAlpha( Av1YuvConverter.ComposeAlpha(
configuration, configuration,
frameBuffer, frameBuffer,
destination.Frames.RootFrame, destination.Frames.RootFrame.PixelBuffer.GetRegion(destination.Frames.RootFrame.Bounds),
destination.Size, destination.Size,
destination.Bounds, destination.Bounds,
false); false,
default);
for (int y = 0; y < destinationHeight; y++) for (int y = 0; y < destinationHeight; y++)
{ {
@ -1259,10 +1499,11 @@ public class Av1YuvConverterTests
Av1YuvConverter.ComposeAlpha( Av1YuvConverter.ComposeAlpha(
Configuration.Default, Configuration.Default,
frameBuffer, frameBuffer,
destination.Frames.RootFrame, destination.Frames.RootFrame.PixelBuffer.GetRegion(destination.Frames.RootFrame.Bounds),
destination.Size, destination.Size,
destination.Bounds, destination.Bounds,
false); false,
default);
Span<Rgba64> actual = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); Span<Rgba64> actual = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0);
Assert.Equal((ushort)0, actual[0].A); Assert.Equal((ushort)0, actual[0].A);

14
tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs

@ -134,16 +134,6 @@ public class ObuFrameHeaderTests
writer.WriteSequenceFrame(stream, sequence, frame, tiles); writer.WriteSequenceFrame(stream, sequence, frame, tiles);
byte[] payload = stream.ToArray(); byte[] payload = stream.ToArray();
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Heif",
"Av1",
nameof(this.DecodeSmallFrameWithLargeSequenceMaximum));
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(Path.Combine(outputDirectory, "original.bit"), originalPayload.ToArray());
File.WriteAllBytes(Path.Combine(outputDirectory, "large-maximum.bit"), payload);
using Av1Decoder originalDecoder = new(Configuration.Default); using Av1Decoder originalDecoder = new(Configuration.Default);
using Av1FrameBuffer<byte> expected = originalDecoder.DecodeFrameBuffer(originalPayload, null, null, out _); using Av1FrameBuffer<byte> expected = originalDecoder.DecodeFrameBuffer(originalPayload, null, null, out _);
using Av1Decoder decoder = new(Configuration.Default); using Av1Decoder decoder = new(Configuration.Default);
@ -154,7 +144,6 @@ public class ObuFrameHeaderTests
Assert.Equal(4, actual.MaxHeight); Assert.Equal(4, actual.MaxHeight);
Assert.Equal(expected.ColorFormat, actual.ColorFormat); Assert.Equal(expected.ColorFormat, actual.ColorFormat);
Assert.Equal(expected.BitDepth, actual.BitDepth); Assert.Equal(expected.BitDepth, actual.BitDepth);
using FileStream decodedPlanes = File.Create(Path.Combine(outputDirectory, "managed.yuv"));
for (int planeIndex = 0; planeIndex < sequence.ColorConfig.PlaneCount; planeIndex++) for (int planeIndex = 0; planeIndex < sequence.ColorConfig.PlaneCount; planeIndex++)
{ {
Av1Plane plane = (Av1Plane)planeIndex; Av1Plane plane = (Av1Plane)planeIndex;
@ -173,7 +162,6 @@ public class ObuFrameHeaderTests
.Slice(actual.OriginX >> subX, width); .Slice(actual.OriginX >> subX, width);
Assert.True(expectedRow.SequenceEqual(actualRow), $"Plane {planeIndex}, row {row}"); Assert.True(expectedRow.SequenceEqual(actualRow), $"Plane {planeIndex}, row {row}");
decodedPlanes.Write(actualRow);
} }
} }
} }
@ -866,7 +854,7 @@ public class ObuFrameHeaderTests
Assert.Equal(typeof(byte), headerScratch.ElementType); Assert.Equal(typeof(byte), headerScratch.ElementType);
Assert.InRange(headerScratch.Length, 1, TilePayloadLength - 1); Assert.InRange(headerScratch.Length, 1, TilePayloadLength - 1);
TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog); TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);
Assert.Equal(headerScratch.AllocationId, returned.AllocationId); Assert.Equal(headerScratch.HashCodeOfBuffer, returned.HashCodeOfBuffer);
Assert.True(stream.GetBuffer().AsSpan((int)stream.Length - TilePayloadLength, TilePayloadLength).SequenceEqual(tileData)); Assert.True(stream.GetBuffer().AsSpan((int)stream.Length - TilePayloadLength, TilePayloadLength).SequenceEqual(tileData));
} }

27
tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameLifecycleTests.cs

@ -5,6 +5,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
@ -105,8 +106,30 @@ public class ObuFrameLifecycleTests
// three-bit slot index, and the required trailing-one bit; no tile-group OBU follows it. // three-bit slot index, and the required trailing-one bit; no tile-group OBU follows it.
byte[] showExistingFrame = [0x1A, 0x01, 0x88]; byte[] showExistingFrame = [0x1A, 0x01, 0x88];
using Av1Decoder decoder = new(Configuration.Default, ProgressiveOperatingPointIndex); using Av1Decoder decoder = new(Configuration.Default, ProgressiveOperatingPointIndex);
using ImageFrame<Rgba32> reconstructed = decoder.DecodeSequenceFrame<Rgba32>(bitStream, null, null); using ImageFrame<Rgba32> reconstructed = new(Configuration.Default, ProgressiveImageWidth, ProgressiveImageHeight);
using ImageFrame<Rgba32> existing = decoder.DecodeSequenceFrame<Rgba32>(showExistingFrame, null, null); decoder.DecodeSequenceFrame(
bitStream,
null,
null,
reconstructed.Size,
reconstructed.Bounds,
reconstructed.PixelBuffer.GetRegion(reconstructed.Bounds),
default,
null,
null,
false);
using ImageFrame<Rgba32> existing = new(Configuration.Default, ProgressiveImageWidth, ProgressiveImageHeight);
decoder.DecodeSequenceFrame(
showExistingFrame,
null,
null,
existing.Size,
existing.Bounds,
existing.PixelBuffer.GetRegion(existing.Bounds),
default,
null,
null,
false);
Assert.Equal(reconstructed.Size, existing.Size); Assert.Equal(reconstructed.Size, existing.Size);
for (int row = 0; row < reconstructed.Height; row++) for (int row = 0; row < reconstructed.Height; row++)

365
tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs

@ -2,8 +2,6 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers.Binary; using System.Buffers.Binary;
using SixLabors.ImageSharp.ColorProfiles;
using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Heif; using SixLabors.ImageSharp.Formats.Heif;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
@ -11,8 +9,8 @@ using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.ColorProfiles.Icc;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
namespace SixLabors.ImageSharp.Tests.Formats.Heif; namespace SixLabors.ImageSharp.Tests.Formats.Heif;
@ -22,19 +20,86 @@ public class HeifDecoderTests
{ {
private const uint UnknownBoxType = 0x74657374U; private const uint UnknownBoxType = 0x74657374U;
private static ReadOnlySpan<byte> MalformedJpegApp13 => /// <summary>
[ /// Decodes the AVIF corpus through the public decoder and compares every frame with the independent decoder.
0xFF, 0xED, /// </summary>
0x00, 0x1D, [Theory]
(byte)'P', (byte)'h', (byte)'o', (byte)'t', (byte)'o', (byte)'s', (byte)'h', (byte)'o', (byte)'p', (byte)' ', (byte)'3', (byte)'.', [WithFile(TestImages.Heif.IrvineAvif, PixelTypes.Rgba32)]
(byte)'0', 0x00, [WithFile(TestImages.Heif.XnConvert, PixelTypes.Rgba32)]
(byte)'B', (byte)'a', (byte)'d', (byte)'R', (byte)'e', (byte)'s', (byte)'o', (byte)'u', (byte)'r', (byte)'c', (byte)'e', (byte)'!', [WithFile(TestImages.Heif.Orange4x4, PixelTypes.Rgba32)]
(byte)'!' [WithFile(TestImages.Heif.ParisIccExifXmpAvif, PixelTypes.Rgba32)]
]; [WithFile(TestImages.Heif.PerceptualIccAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.PerceptualIccGridAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.PerceptualIccSequenceAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.DuckyRommIccAlphaAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Animated8Bit, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Animated8BitWithAudio, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Animated8BitWithAlphaExifXmp, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1Deblocking8BitAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1Progressive8BitAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1ScaledReferenceAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1ScaledReferenceSelectedLayerAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1AverageCompoundSequenceAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1DistanceWeightedCompoundSequenceAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1WedgeCompoundSequenceAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1DifferenceWeightedCompoundSequenceAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1InterIntraSequenceAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1ObmcSequenceAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1LocalWarpSequenceAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1GlobalWarpSequenceAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1Cdef8BitAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1Profile8BitMonochromeAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1Profile8Bit420Avif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1Profile8Bit422Avif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1Profile8Bit444Avif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1Palette8BitAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1IntraBlockCopy8BitAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1Lossless8BitAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1SuperResolution8BitAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Av1Restoration8BitAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.Animated12BitWithKeyframes, PixelTypes.Rgba64)]
[WithFile(TestImages.Heif.Av1Deblocking10BitAvif, PixelTypes.Rgba64)]
[WithFile(TestImages.Heif.Av1Deblocking12BitAvif, PixelTypes.Rgba64)]
[WithFile(TestImages.Heif.Av1Cdef10BitAvif, PixelTypes.Rgba64)]
[WithFile(TestImages.Heif.Av1Cdef12BitAvif, PixelTypes.Rgba64)]
[WithFile(TestImages.Heif.Av1Profile10BitMonochromeAvif, PixelTypes.Rgba64)]
[WithFile(TestImages.Heif.Av1Profile10Bit420Avif, PixelTypes.Rgba64)]
[WithFile(TestImages.Heif.Av1Profile10Bit422Avif, PixelTypes.Rgba64)]
[WithFile(TestImages.Heif.Av1Profile10Bit444Avif, PixelTypes.Rgba64)]
[WithFile(TestImages.Heif.Av1Profile12BitMonochromeAvif, PixelTypes.Rgba64)]
[WithFile(TestImages.Heif.Av1Profile12Bit420Avif, PixelTypes.Rgba64)]
[WithFile(TestImages.Heif.Av1Profile12Bit422Avif, PixelTypes.Rgba64)]
[WithFile(TestImages.Heif.Av1Profile12Bit444Avif, PixelTypes.Rgba64)]
[WithFile(TestImages.Heif.Av1IntraBlockCopy10BitAvif, PixelTypes.Rgba64)]
[WithFile(TestImages.Heif.Av1IntraBlockCopy12BitAvif, PixelTypes.Rgba64)]
[WithFile(TestImages.Heif.Av1Lossless10BitAvif, PixelTypes.Rgba64)]
[WithFile(TestImages.Heif.Av1Lossless12BitAvif, PixelTypes.Rgba64)]
[WithFile(TestImages.Heif.Av1SuperResolution10BitAvif, PixelTypes.Rgba64)]
[WithFile(TestImages.Heif.Av1SuperResolution12BitAvif, PixelTypes.Rgba64)]
[WithFile(TestImages.Heif.Av1Restoration10BitAvif, PixelTypes.Rgba64)]
[WithFile(TestImages.Heif.Av1Restoration12BitAvif, PixelTypes.Rgba64)]
public void Decode<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
DecoderOptions options = new() { ColorProfileHandling = ColorProfileHandling.Preserve };
using Image<TPixel> image = provider.GetImage(HeifDecoder.Instance, options);
if (image.Frames.Count == 1)
{
image.DebugSave(provider, extension: "png", encoder: new PngEncoder());
image.CompareToReferenceOutput(ImageComparer.Exact, provider);
}
else
{
image.DebugSaveMultiFrame(provider, encoder: new PngEncoder());
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
}
}
[Theory] [Theory]
[InlineData(TestImages.Heif.IrvineAvif, HeifCompressionMethod.Av1, HeifBitDepth.Bit8, 480, 640)] [InlineData(TestImages.Heif.IrvineAvif, HeifBitDepth.Bit8, 480, 640)]
public void Identify(string imagePath, HeifCompressionMethod compressionMethod, HeifBitDepth bitDepth, int width, int height) public void Identify(string imagePath, HeifBitDepth bitDepth, int width, int height)
{ {
TestFile testFile = TestFile.Create(imagePath); TestFile testFile = TestFile.Create(imagePath);
using MemoryStream stream = new(testFile.Bytes, false); using MemoryStream stream = new(testFile.Bytes, false);
@ -44,7 +109,6 @@ public class HeifDecoderTests
Assert.NotNull(imageInfo); Assert.NotNull(imageInfo);
Assert.Equal(HeifFormat.Instance, imageInfo.Metadata.DecodedImageFormat); Assert.Equal(HeifFormat.Instance, imageInfo.Metadata.DecodedImageFormat);
Assert.Equal(compressionMethod, heifMetadata.CompressionMethod);
Assert.Equal(bitDepth, heifMetadata.BitDepth); Assert.Equal(bitDepth, heifMetadata.BitDepth);
Assert.Equal(width, imageInfo.Width); Assert.Equal(width, imageInfo.Width);
Assert.Equal(height, imageInfo.Height); Assert.Equal(height, imageInfo.Height);
@ -120,7 +184,7 @@ public class HeifDecoderTests
/// </summary> /// </summary>
[Theory] [Theory]
[WithFile(TestImages.Heif.ParisIccExifXmpAvif, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.ParisIccExifXmpAvif, PixelTypes.Rgba32)]
public void DecodeAvifPreservesEmbeddedIccProfile<TPixel>(TestImageProvider<TPixel> provider) public void IccPreserve<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve };
@ -131,6 +195,8 @@ public class HeifDecoderTests
Assert.NotNull(preserved.Metadata.IccProfile); Assert.NotNull(preserved.Metadata.IccProfile);
Assert.NotNull(expectedPreserved.Metadata.IccProfile); Assert.NotNull(expectedPreserved.Metadata.IccProfile);
Assert.Equal(expectedPreserved.Metadata.IccProfile.ToByteArray(), preserved.Metadata.IccProfile.ToByteArray()); Assert.Equal(expectedPreserved.Metadata.IccProfile.ToByteArray(), preserved.Metadata.IccProfile.ToByteArray());
preserved.DebugSave(provider, extension: "png", encoder: new PngEncoder());
preserved.CompareToReferenceOutput(ImageComparer.Exact, provider);
} }
/// <summary> /// <summary>
@ -138,7 +204,7 @@ public class HeifDecoderTests
/// </summary> /// </summary>
[Theory] [Theory]
[WithFile(TestImages.Heif.PerceptualIccAvif, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.PerceptualIccAvif, PixelTypes.Rgba32)]
public void DecodeAvifConvertsEmbeddedNonSrgbIccProfile<TPixel>(TestImageProvider<TPixel> provider) public void IccConvert<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve };
@ -146,22 +212,13 @@ public class HeifDecoderTests
using Image<TPixel> preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); using Image<TPixel> preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions);
using Image<TPixel> converted = provider.GetImage(HeifDecoder.Instance, convertOptions); using Image<TPixel> converted = provider.GetImage(HeifDecoder.Instance, convertOptions);
using Image<TPixel> expected = Image.Load<TPixel>(convertOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes);
Assert.NotNull(preserved.Metadata.IccProfile); Assert.NotNull(preserved.Metadata.IccProfile);
Assert.Null(converted.Metadata.IccProfile); Assert.Null(converted.Metadata.IccProfile);
Assert.NotEmpty(ImageComparer.Exact.CompareImages(preserved, converted)); Assert.NotEmpty(ImageComparer.Exact.CompareImages(preserved, converted));
// The decoded metadata retains the AVIF source matrix, which PNG cannot represent. The debug output exists converted.DebugSave(provider, extension: "png", encoder: new PngEncoder());
// only to inspect converted pixels, so omit metadata without altering the image under test. converted.CompareToReferenceOutput(ImageComparer.Exact, provider);
converted.DebugSave(
provider,
new PngEncoder { SkipMetadata = true },
testOutputDetails: "IccConverted");
// The PNG is the independent RGB source used by libavif's avifenc. A tolerant comparison accounts for the
// AV1 loss while proving the AVIF ICC stage produces the same target-profile interpretation.
ImageComparer.TolerantPercentage(1F, 20).VerifySimilarity(expected, converted);
} }
/// <summary> /// <summary>
@ -169,7 +226,7 @@ public class HeifDecoderTests
/// </summary> /// </summary>
[Theory] [Theory]
[WithFile(TestImages.Heif.PerceptualIccGridAvif, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.PerceptualIccGridAvif, PixelTypes.Rgba32)]
public void DecodeAvifGridConvertsEmbeddedNonSrgbIccProfile<TPixel>(TestImageProvider<TPixel> provider) public void IccConvertGrid<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve };
@ -178,14 +235,14 @@ public class HeifDecoderTests
using Image<TPixel> preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); using Image<TPixel> preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions);
using Image<TPixel> converted = provider.GetImage(HeifDecoder.Instance, convertOptions); using Image<TPixel> converted = provider.GetImage(HeifDecoder.Instance, convertOptions);
using Image<TPixel> expectedPreserved = Image.Load<TPixel>(preserveOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes); using Image<TPixel> expectedPreserved = Image.Load<TPixel>(preserveOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes);
using Image<TPixel> expected = Image.Load<TPixel>(convertOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes);
IccProfile preservedIccProfile = Assert.IsType<IccProfile>(preserved.Metadata.IccProfile); IccProfile preservedIccProfile = Assert.IsType<IccProfile>(preserved.Metadata.IccProfile);
IccProfile expectedIccProfile = Assert.IsType<IccProfile>(expectedPreserved.Metadata.IccProfile); IccProfile expectedIccProfile = Assert.IsType<IccProfile>(expectedPreserved.Metadata.IccProfile);
Assert.Null(converted.Metadata.IccProfile); Assert.Null(converted.Metadata.IccProfile);
Assert.Equal(expectedIccProfile.ToByteArray(), preservedIccProfile.ToByteArray()); Assert.Equal(expectedIccProfile.ToByteArray(), preservedIccProfile.ToByteArray());
Assert.NotEmpty(ImageComparer.Exact.CompareImages(preserved, converted)); Assert.NotEmpty(ImageComparer.Exact.CompareImages(preserved, converted));
ImageComparer.TolerantPercentage(1F, 20).VerifySimilarity(expected, converted); converted.DebugSave(provider, extension: "png", encoder: new PngEncoder());
converted.CompareToReferenceOutput(ImageComparer.Exact, provider);
} }
/// <summary> /// <summary>
@ -193,7 +250,7 @@ public class HeifDecoderTests
/// </summary> /// </summary>
[Theory] [Theory]
[WithFile(TestImages.Heif.PerceptualIccSequenceAvif, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.PerceptualIccSequenceAvif, PixelTypes.Rgba32)]
public void DecodeAvifSequenceConvertsEveryFrameWithEmbeddedNonSrgbIccProfile<TPixel>(TestImageProvider<TPixel> provider) public void IccConvertSequence<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve };
@ -202,7 +259,6 @@ public class HeifDecoderTests
using Image<TPixel> preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); using Image<TPixel> preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions);
using Image<TPixel> converted = provider.GetImage(HeifDecoder.Instance, convertOptions); using Image<TPixel> converted = provider.GetImage(HeifDecoder.Instance, convertOptions);
using Image<TPixel> expectedPreserved = Image.Load<TPixel>(preserveOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes); using Image<TPixel> expectedPreserved = Image.Load<TPixel>(preserveOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes);
using Image<TPixel> expected = Image.Load<TPixel>(convertOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes);
Assert.Equal(2, preserved.Frames.Count); Assert.Equal(2, preserved.Frames.Count);
Assert.Equal(preserved.Frames.Count, converted.Frames.Count); Assert.Equal(preserved.Frames.Count, converted.Frames.Count);
@ -211,55 +267,23 @@ public class HeifDecoderTests
Assert.Null(converted.Metadata.IccProfile); Assert.Null(converted.Metadata.IccProfile);
Assert.Equal(expectedIccProfile.ToByteArray(), preservedIccProfile.ToByteArray()); Assert.Equal(expectedIccProfile.ToByteArray(), preservedIccProfile.ToByteArray());
for (int i = 0; i < converted.Frames.Count; i++) converted.DebugSaveMultiFrame(provider, encoder: new PngEncoder());
{ converted.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
Assert.False(ImageComparer.Exact.CompareImagesOrFrames(i, preserved.Frames[i], converted.Frames[i]).IsEmpty);
Assert.True(ImageComparer.TolerantPercentage(1F, 20).CompareImagesOrFrames(i, expected.Frames.RootFrame, converted.Frames[i]).IsEmpty);
}
} }
/// <summary> /// <summary>
/// Verifies that non-sRGB ICC conversion follows auxiliary-alpha composition and preserves the composed alpha values. /// Verifies that non-sRGB ICC conversion follows auxiliary-alpha composition and preserves the composed alpha values.
/// </summary> /// </summary>
[Fact] [Theory]
public void DecodeAvifAlphaImageConvertsEmbeddedIccProfileWithoutChangingAlpha() [WithFile(TestImages.Heif.DuckyRommIccAlphaAvif, PixelTypes.Rgba32)]
public void IccConvertAlpha(TestImageProvider<Rgba32> provider)
{ {
DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve };
DecoderOptions convertOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert }; DecoderOptions convertOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert };
byte[] encoded = TestFile.Create(TestImages.Heif.DuckyRommIccAlphaAvif).Bytes; using Image<Rgba32> decoded = provider.GetImage(HeifDecoder.Instance, convertOptions);
using Image<Rgba32> preserved = Image.Load<Rgba32>(preserveOptions, encoded);
using Image<Rgba32> converted = Image.Load<Rgba32>(convertOptions, encoded);
using Image<Rgba32> expected = preserved.Clone();
ColorProfileConverter converter = new(new ColorConversionOptions
{
SourceIccProfile = expected.Metadata.IccProfile,
TargetIccProfile = CompactSrgbV4Profile.Profile,
MemoryAllocator = expected.Configuration.MemoryAllocator,
});
// Build the oracle from the fully composed preserved decode so that only ICC ordering and alpha retention decoded.DebugSave(provider, extension: "png", encoder: new PngEncoder());
// are under test; the independently encoded AV1 color and alpha payloads remain identical in both paths. decoded.CompareToReferenceOutput(ImageComparer.Exact, provider);
converter.Convert(expected); Assert.Null(decoded.Metadata.IccProfile);
Assert.NotNull(preserved.Metadata.IccProfile);
Assert.Null(converted.Metadata.IccProfile);
Assert.Equal(TestIccProfiles.GetProfile(TestIccProfiles.RommRgb).ToByteArray(), preserved.Metadata.IccProfile.ToByteArray());
Assert.NotEmpty(ImageComparer.Exact.CompareImages(preserved, converted));
for (int y = 0; y < converted.Height; y++)
{
Span<Rgba32> preservedRow = preserved.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
Span<Rgba32> convertedRow = converted.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
for (int x = 0; x < convertedRow.Length; x++)
{
Assert.Equal(preservedRow[x].A, convertedRow[x].A);
}
}
ImageComparer.Exact.VerifySimilarity(expected, converted);
} }
/// <summary> /// <summary>
@ -270,7 +294,7 @@ public class HeifDecoderTests
[WithFile(TestImages.Heif.PerceptualIccGridAvif, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.PerceptualIccGridAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.PerceptualIccSequenceAvif, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.PerceptualIccSequenceAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.DuckyRommIccAlphaAvif, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.DuckyRommIccAlphaAvif, PixelTypes.Rgba32)]
public void DecodeAvifRetainsNonSrgbIccProfileWhenCompacting<TPixel>(TestImageProvider<TPixel> provider) public void IccCompactNonSrgb<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve };
@ -283,6 +307,16 @@ public class HeifDecoderTests
Assert.NotNull(compact.Metadata.IccProfile); Assert.NotNull(compact.Metadata.IccProfile);
Assert.Equal(preserved.Metadata.IccProfile.ToByteArray(), compact.Metadata.IccProfile.ToByteArray()); Assert.Equal(preserved.Metadata.IccProfile.ToByteArray(), compact.Metadata.IccProfile.ToByteArray());
Assert.Empty(ImageComparer.Exact.CompareImages(preserved, compact)); Assert.Empty(ImageComparer.Exact.CompareImages(preserved, compact));
if (compact.Frames.Count == 1)
{
compact.DebugSave(provider, extension: "png", encoder: new PngEncoder());
compact.CompareToReferenceOutput(ImageComparer.Exact, provider);
}
else
{
compact.DebugSaveMultiFrame(provider, encoder: new PngEncoder());
compact.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
}
} }
/// <summary> /// <summary>
@ -290,7 +324,7 @@ public class HeifDecoderTests
/// </summary> /// </summary>
[Theory] [Theory]
[WithFile(TestImages.Heif.ParisIccExifXmpAvif, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.ParisIccExifXmpAvif, PixelTypes.Rgba32)]
public void DecodeAvifCompactsCanonicalSrgbIccProfile<TPixel>(TestImageProvider<TPixel> provider) public void IccCompactSrgb<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve };
@ -302,6 +336,8 @@ public class HeifDecoderTests
Assert.NotNull(preserved.Metadata.IccProfile); Assert.NotNull(preserved.Metadata.IccProfile);
Assert.Null(compact.Metadata.IccProfile); Assert.Null(compact.Metadata.IccProfile);
Assert.Empty(ImageComparer.Exact.CompareImages(preserved, compact)); Assert.Empty(ImageComparer.Exact.CompareImages(preserved, compact));
compact.DebugSave(provider, extension: "png", encoder: new PngEncoder());
compact.CompareToReferenceOutput(ImageComparer.Exact, provider);
} }
/// <summary> /// <summary>
@ -312,7 +348,7 @@ public class HeifDecoderTests
[WithFile(TestImages.Heif.PerceptualIccGridAvif, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.PerceptualIccGridAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.PerceptualIccSequenceAvif, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.PerceptualIccSequenceAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.DuckyRommIccAlphaAvif, PixelTypes.Rgba32)] [WithFile(TestImages.Heif.DuckyRommIccAlphaAvif, PixelTypes.Rgba32)]
public void DecodeAvifSkipsEmbeddedIccProfileWithMetadata<TPixel>(TestImageProvider<TPixel> provider) public void IccSkipMetadata<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
DecoderOptions options = new() DecoderOptions options = new()
@ -324,12 +360,22 @@ public class HeifDecoderTests
using Image<TPixel> image = provider.GetImage(HeifDecoder.Instance, options); using Image<TPixel> image = provider.GetImage(HeifDecoder.Instance, options);
Assert.Null(image.Metadata.IccProfile); Assert.Null(image.Metadata.IccProfile);
if (image.Frames.Count == 1)
{
image.DebugSave(provider, extension: "png", encoder: new PngEncoder());
image.CompareToReferenceOutput(ImageComparer.Exact, provider);
}
else
{
image.DebugSaveMultiFrame(provider, encoder: new PngEncoder());
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
}
} }
[Fact] [Fact]
public void DecodeIgnoresUnknownTopLevelBox() public void DecodeIgnoresUnknownTopLevelBox()
{ {
byte[] data = CreateLegacyJpegContainer(); byte[] data = CreateAv1Container();
data = InsertBytes(data, data.Length, CreateUnknownBox()); data = InsertBytes(data, data.Length, CreateUnknownBox());
using Image<Rgba32> image = Image.Load<Rgba32>(data); using Image<Rgba32> image = Image.Load<Rgba32>(data);
@ -369,58 +415,6 @@ public class HeifDecoderTests
} }
} }
[Fact]
public void DecodePropagatesStrictValidationToLegacyJpegItems()
{
byte[] data = CreateContainerWithMalformedJpegMetadata();
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidImageContentException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, data);
});
}
[Theory]
[InlineData(SegmentIntegrityHandling.IgnoreAncillary)]
[InlineData(SegmentIntegrityHandling.IgnoreImageData)]
public void DecodePropagatesRecoverableMetadataValidationToLegacyJpegItems(SegmentIntegrityHandling handling)
{
byte[] data = CreateContainerWithMalformedJpegMetadata();
DecoderOptions options = new() { SegmentIntegrityHandling = handling };
using Image<Rgba32> image = Image.Load<Rgba32>(options, data);
Assert.Equal(new Size(2, 3), image.Size);
}
[Fact]
public void DecodePropagatesSkipMetadataToLegacyJpegItems()
{
byte[] data = CreateContainerWithMalformedJpegMetadata();
DecoderOptions options = new()
{
SkipMetadata = true,
SegmentIntegrityHandling = SegmentIntegrityHandling.Strict
};
using Image<Rgba32> image = Image.Load<Rgba32>(options, data);
Assert.Equal(new Size(2, 3), image.Size);
}
[Fact]
public void DecodePropagatesConfigurationToLegacyJpegItems()
{
byte[] data = CreateLegacyJpegContainer();
Configuration configuration = Configuration.CreateDefaultInstance();
DecoderOptions options = new() { Configuration = configuration };
using Image<Rgba32> image = Image.Load<Rgba32>(options, data);
Assert.Same(configuration, image.Configuration);
}
/// <summary> /// <summary>
/// Verifies that invalid optional alpha payloads remain fatal when image-data errors cannot be ignored. /// Verifies that invalid optional alpha payloads remain fatal when image-data errors cannot be ignored.
/// </summary> /// </summary>
@ -555,7 +549,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyIgnoresUnknownMetadataBox() public void IdentifyIgnoresUnknownMetadataBox()
{ {
byte[] data = CreateLegacyJpegContainer(); byte[] data = CreateAv1Container();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
data = InsertBytes(data, metaOffset + metaSize, CreateUnknownBox()); data = InsertBytes(data, metaOffset + metaSize, CreateUnknownBox());
@ -698,10 +692,9 @@ public class HeifDecoderTests
[Theory] [Theory]
[InlineData(Heif4CharCode.Mif1)] [InlineData(Heif4CharCode.Mif1)]
[InlineData(Heif4CharCode.Avif)] [InlineData(Heif4CharCode.Avif)]
[InlineData(Heif4CharCode.Jpeg)]
public void DetectorRecognizesSupportedStillImageMajorBrand(Heif4CharCode brand) public void DetectorRecognizesSupportedStillImageMajorBrand(Heif4CharCode brand)
{ {
byte[] data = CreateLegacyJpegContainer(); byte[] data = CreateAv1Container();
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand);
HeifImageFormatDetector detector = new(); HeifImageFormatDetector detector = new();
@ -715,7 +708,7 @@ public class HeifDecoderTests
[InlineData(Heif4CharCode.Avis)] [InlineData(Heif4CharCode.Avis)]
public void DetectorRecognizesSupportedSequenceMajorBrand(Heif4CharCode brand) public void DetectorRecognizesSupportedSequenceMajorBrand(Heif4CharCode brand)
{ {
byte[] data = CreateLegacyJpegContainer(); byte[] data = CreateAv1Container();
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand);
HeifImageFormatDetector detector = new(); HeifImageFormatDetector detector = new();
@ -741,21 +734,10 @@ public class HeifDecoderTests
Assert.Same(HeifFormat.Instance, format); Assert.Same(HeifFormat.Instance, format);
} }
[Theory]
[InlineData(Heif4CharCode.Jpgs)]
public void DetectorRejectsUnsupportedSequenceMajorBrand(Heif4CharCode brand)
{
byte[] data = CreateLegacyJpegContainer();
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand);
HeifImageFormatDetector detector = new();
Assert.False(detector.TryDetectFormat(data.AsSpan(0, detector.HeaderSize), out _));
}
[Fact] [Fact]
public void IdentifyRejectsUnsupportedBrands() public void IdentifyRejectsUnsupportedBrands()
{ {
byte[] data = CreateLegacyJpegContainer(); byte[] data = CreateAv1Container();
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), UnknownBoxType); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), UnknownBoxType);
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(16), UnknownBoxType); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(16), UnknownBoxType);
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(20), UnknownBoxType); BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(20), UnknownBoxType);
@ -767,7 +749,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyAcceptsExtendedSizeTopLevelBox() public void IdentifyAcceptsExtendedSizeTopLevelBox()
{ {
byte[] data = CreateLegacyJpegContainer(); byte[] data = CreateAv1Container();
byte[] box = new byte[16]; byte[] box = new byte[16];
BinaryPrimitives.WriteUInt32BigEndian(box, 1); BinaryPrimitives.WriteUInt32BigEndian(box, 1);
BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), UnknownBoxType); BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), UnknownBoxType);
@ -782,7 +764,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyAcceptsUuidTopLevelBox() public void IdentifyAcceptsUuidTopLevelBox()
{ {
byte[] data = CreateLegacyJpegContainer(); byte[] data = CreateAv1Container();
byte[] box = new byte[24]; byte[] box = new byte[24];
BinaryPrimitives.WriteUInt32BigEndian(box, (uint)box.Length); BinaryPrimitives.WriteUInt32BigEndian(box, (uint)box.Length);
BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), (uint)Heif4CharCode.Uuid); BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), (uint)Heif4CharCode.Uuid);
@ -796,7 +778,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyAcceptsSizeZeroTopLevelBox() public void IdentifyAcceptsSizeZeroTopLevelBox()
{ {
byte[] data = CreateLegacyJpegContainer(); byte[] data = CreateAv1Container();
byte[] box = CreateUnknownBox(); byte[] box = CreateUnknownBox();
BinaryPrimitives.WriteUInt32BigEndian(box, 0); BinaryPrimitives.WriteUInt32BigEndian(box, 0);
data = InsertBytes(data, data.Length, box); data = InsertBytes(data, data.Length, box);
@ -809,7 +791,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyAcceptsExtendedSizeItemInfoEntry() public void IdentifyAcceptsExtendedSizeItemInfoEntry()
{ {
byte[] data = CreateLegacyJpegContainer(); byte[] data = CreateAv1Container();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12); int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12);
@ -829,7 +811,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyRejectsSizeZeroMetadataChild() public void IdentifyRejectsSizeZeroMetadataChild()
{ {
byte[] data = CreateLegacyJpegContainer(); byte[] data = CreateAv1Container();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
byte[] box = CreateUnknownBox(); byte[] box = CreateUnknownBox();
@ -843,7 +825,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyRejectsMetadataChildBeyondParent() public void IdentifyRejectsMetadataChildBeyondParent()
{ {
byte[] data = CreateLegacyJpegContainer(); byte[] data = CreateAv1Container();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
byte[] box = CreateUnknownBox(); byte[] box = CreateUnknownBox();
@ -857,7 +839,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyRejectsItemInfoEntryBeyondParent() public void IdentifyRejectsItemInfoEntryBeyondParent()
{ {
byte[] data = CreateLegacyJpegContainer(); byte[] data = CreateAv1Container();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12); int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12);
@ -871,7 +853,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyRejectsBoxSmallerThanHeader() public void IdentifyRejectsBoxSmallerThanHeader()
{ {
byte[] data = CreateLegacyJpegContainer(); byte[] data = CreateAv1Container();
byte[] box = CreateUnknownBox(); byte[] box = CreateUnknownBox();
BinaryPrimitives.WriteUInt32BigEndian(box, 4); BinaryPrimitives.WriteUInt32BigEndian(box, 4);
data = InsertBytes(data, data.Length, box); data = InsertBytes(data, data.Length, box);
@ -882,7 +864,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyRejectsTruncatedExtendedSizeHeader() public void IdentifyRejectsTruncatedExtendedSizeHeader()
{ {
byte[] data = CreateLegacyJpegContainer(); byte[] data = CreateAv1Container();
byte[] box = new byte[12]; byte[] box = new byte[12];
BinaryPrimitives.WriteUInt32BigEndian(box, 1); BinaryPrimitives.WriteUInt32BigEndian(box, 1);
BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), UnknownBoxType); BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), UnknownBoxType);
@ -894,7 +876,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyRejectsTruncatedUuidHeader() public void IdentifyRejectsTruncatedUuidHeader()
{ {
byte[] data = CreateLegacyJpegContainer(); byte[] data = CreateAv1Container();
byte[] box = new byte[16]; byte[] box = new byte[16];
BinaryPrimitives.WriteUInt32BigEndian(box, 24); BinaryPrimitives.WriteUInt32BigEndian(box, 24);
BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), (uint)Heif4CharCode.Uuid); BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), (uint)Heif4CharCode.Uuid);
@ -906,7 +888,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyAcceptsItemPropertiesBeforeItemInfo() public void IdentifyAcceptsItemPropertiesBeforeItemInfo()
{ {
byte[] data = CreateLegacyJpegContainer(); byte[] data = CreateAv1Container();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12); int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12);
@ -922,7 +904,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyAcceptsItemLocationBeforeItemInfo() public void IdentifyAcceptsItemLocationBeforeItemInfo()
{ {
byte[] data = CreateLegacyJpegContainer(); byte[] data = CreateAv1Container();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12); int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12);
@ -938,7 +920,7 @@ public class HeifDecoderTests
[Fact] [Fact]
public void IdentifyRejectsDuplicateUniqueMetadataBox() public void IdentifyRejectsDuplicateUniqueMetadataBox()
{ {
byte[] data = CreateLegacyJpegContainer(); byte[] data = CreateAv1Container();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int pitmOffset = FindBoxOffset(data, Heif4CharCode.Pitm, metaOffset + 12, metaSize - 12); int pitmOffset = FindBoxOffset(data, Heif4CharCode.Pitm, metaOffset + 12, metaSize - 12);
@ -949,11 +931,11 @@ public class HeifDecoderTests
Assert.Throws<InvalidImageContentException>(() => Image.Identify(data)); Assert.Throws<InvalidImageContentException>(() => Image.Identify(data));
} }
private static byte[] CreateLegacyJpegContainer() private static byte[] CreateAv1Container()
{ {
using Image<Rgba32> image = new(2, 3); using Image<Rgb24> image = new(2, 3);
using MemoryStream stream = new(); using MemoryStream stream = new();
image.Save(stream, new HeifEncoder { CompressionMethod = HeifCompressionMethod.LegacyJpeg }); image.Save(stream, new HeifEncoder());
return stream.ToArray(); return stream.ToArray();
} }
@ -962,7 +944,7 @@ public class HeifDecoderTests
private static byte[] CreateContainerWithProperty(ReadOnlySpan<byte> property, bool essential) private static byte[] CreateContainerWithProperty(ReadOnlySpan<byte> property, bool essential)
{ {
byte[] data = CreateLegacyJpegContainer(); byte[] data = CreateAv1Container();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int iprpOffset = FindBoxOffset(data, Heif4CharCode.Iprp, metaOffset + 12, metaSize - 12); int iprpOffset = FindBoxOffset(data, Heif4CharCode.Iprp, metaOffset + 12, metaSize - 12);
@ -971,18 +953,26 @@ public class HeifDecoderTests
int ipcoSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ipcoOffset)); int ipcoSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ipcoOffset));
int ipmaOffset = FindBoxOffset(data, Heif4CharCode.Ipma, iprpOffset + 8, iprpSize - 8); int ipmaOffset = FindBoxOffset(data, Heif4CharCode.Ipma, iprpOffset + 8, iprpSize - 8);
// Insert the property before ipma so its one-based index is 2 and all parent box sizes remain explicit. // Count existing AV1 properties before appending the test property; its association is one-based.
int propertyIndex = 1;
for (int offset = ipcoOffset + 8; offset < ipcoOffset + ipcoSize;)
{
offset += (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(offset));
propertyIndex++;
}
// Insert before ipma and update each enclosing box size.
data = InsertBytes(data, ipcoOffset + ipcoSize, property); data = InsertBytes(data, ipcoOffset + ipcoSize, property);
IncrementBoxSize(data, metaOffset, property.Length); IncrementBoxSize(data, metaOffset, property.Length);
IncrementBoxSize(data, iprpOffset, property.Length); IncrementBoxSize(data, iprpOffset, property.Length);
IncrementBoxSize(data, ipcoOffset, property.Length); IncrementBoxSize(data, ipcoOffset, property.Length);
ipmaOffset += property.Length; ipmaOffset += property.Length;
// The generated container has one item with one property association; append the inserted property to that entry. // Append the new property association to the sole opaque AV1 item's existing entry.
int associationCountOffset = ipmaOffset + 18; int associationCountOffset = ipmaOffset + 18;
data[associationCountOffset]++; data[associationCountOffset]++;
int associationOffset = ipmaOffset + (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ipmaOffset)); int associationOffset = ipmaOffset + (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ipmaOffset));
byte association = (byte)(2 | (essential ? 0x80 : 0)); byte association = (byte)(propertyIndex | (essential ? 0x80 : 0));
data = InsertBytes(data, associationOffset, new byte[] { association }); data = InsertBytes(data, associationOffset, new byte[] { association });
IncrementBoxSize(data, metaOffset, 1); IncrementBoxSize(data, metaOffset, 1);
IncrementBoxSize(data, iprpOffset, 1); IncrementBoxSize(data, iprpOffset, 1);
@ -990,23 +980,6 @@ public class HeifDecoderTests
return data; return data;
} }
private static byte[] CreateContainerWithMalformedJpegMetadata()
{
byte[] data = CreateLegacyJpegContainer();
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
int itemLocationOffset = FindBoxOffset(data, Heif4CharCode.Iloc, metaOffset + 12, metaSize - 12);
int mediaDataOffset = FindBoxOffset(data, Heif4CharCode.Mdat, 0, data.Length);
// The generated item uses one file-relative extent. Insert the malformed JPEG application segment after its
// start-of-image marker, then update the enclosing media-data size and the exact declared extent length.
data = InsertBytes(data, mediaDataOffset + 10, MalformedJpegApp13);
IncrementBoxSize(data, mediaDataOffset, MalformedJpegApp13.Length);
uint extentLength = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(itemLocationOffset + 32));
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(itemLocationOffset + 32), extentLength + (uint)MalformedJpegApp13.Length);
return data;
}
private static byte[] CreateContainerWithDuplicatePropertyAssociation(ReadOnlySpan<byte> property, bool essential) private static byte[] CreateContainerWithDuplicatePropertyAssociation(ReadOnlySpan<byte> property, bool essential)
{ {
byte[] data = CreateContainerWithProperty(property, essential); byte[] data = CreateContainerWithProperty(property, essential);
@ -1020,7 +993,7 @@ public class HeifDecoderTests
// Repeat the inserted property's one-based index in the existing item entry without changing box structure. // Repeat the inserted property's one-based index in the existing item entry without changing box structure.
data[associationCountOffset]++; data[associationCountOffset]++;
int associationOffset = ipmaOffset + (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ipmaOffset)); int associationOffset = ipmaOffset + (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ipmaOffset));
byte association = (byte)(2 | (essential ? 0x80 : 0)); byte association = data[associationOffset - 1];
data = InsertBytes(data, associationOffset, new byte[] { association }); data = InsertBytes(data, associationOffset, new byte[] { association });
IncrementBoxSize(data, metaOffset, 1); IncrementBoxSize(data, metaOffset, 1);
IncrementBoxSize(data, iprpOffset, 1); IncrementBoxSize(data, iprpOffset, 1);
@ -1137,6 +1110,20 @@ public class HeifDecoderTests
/// <param name="data">The complete mutable HEIF container.</param> /// <param name="data">The complete mutable HEIF container.</param>
/// <param name="itemId">The item whose coded payload is cleared.</param> /// <param name="itemId">The item whose coded payload is cleared.</param>
private static void ClearItemPayload(Span<byte> data, uint itemId) private static void ClearItemPayload(Span<byte> data, uint itemId)
{
foreach (Range extent in GetItemPayloadRanges(data, itemId))
{
data[extent].Clear();
}
}
/// <summary>
/// Locates every file-relative extent for an item without decoding its contents.
/// </summary>
/// <param name="data">The complete HEIF container.</param>
/// <param name="itemId">The item whose extents are selected.</param>
/// <returns>The item's extents in file order.</returns>
private static List<Range> GetItemPayloadRanges(ReadOnlySpan<byte> data, uint itemId)
{ {
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
Assert.True(metaOffset >= 0); Assert.True(metaOffset >= 0);
@ -1161,7 +1148,8 @@ public class HeifDecoderTests
: BinaryPrimitives.ReadUInt16BigEndian(data[offset..]); : BinaryPrimitives.ReadUInt16BigEndian(data[offset..]);
offset += version == 2 ? 4 : 2; offset += version == 2 ? 4 : 2;
bool found = false; List<Range> extents = new();
for (uint itemIndex = 0; itemIndex < itemCount; itemIndex++) for (uint itemIndex = 0; itemIndex < itemCount; itemIndex++)
{ {
uint currentItemId = version == 2 uint currentItemId = version == 2
@ -1189,13 +1177,14 @@ public class HeifDecoderTests
ulong extentLength = ReadVariableUnsigned(data, extentLengthSize, ref offset); ulong extentLength = ReadVariableUnsigned(data, extentLengthSize, ref offset);
if (currentItemId == itemId) if (currentItemId == itemId)
{ {
data.Slice(checked((int)(baseOffset + extentOffset)), checked((int)extentLength)).Clear(); int start = checked((int)(baseOffset + extentOffset));
found = true; extents.Add(new Range(start, checked(start + (int)extentLength)));
} }
} }
} }
Assert.True(found); Assert.NotEmpty(extents);
return extents;
} }
/// <summary> /// <summary>

653
tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs

@ -7,9 +7,11 @@ using System.Text;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Heif; using SixLabors.ImageSharp.Formats.Heif;
using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Color;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp; using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
@ -39,7 +41,6 @@ public class HeifEncoderTests
{ {
HeifEncoder encoder = new(); HeifEncoder encoder = new();
Assert.Equal(HeifCompressionMethod.Av1, encoder.CompressionMethod);
Assert.Null(encoder.Quality); Assert.Null(encoder.Quality);
Assert.Null(encoder.AlphaQuality); Assert.Null(encoder.AlphaQuality);
Assert.Equal(5, encoder.Effort); Assert.Equal(5, encoder.Effort);
@ -85,150 +86,6 @@ public class HeifEncoderTests
Assert.Equal(effort, encoder.Effort); Assert.Equal(effort, encoder.Effort);
} }
[Fact]
public void LegacyJpegAcceptsZeroQuality()
{
using Image<Rgba32> image = new(1, 1);
image[0, 0] = new Rgba32(10, 20, 30);
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.LegacyJpeg,
Quality = 0
};
image.Save(stream, encoder);
Assert.NotEqual(0, stream.Length);
stream.Position = 0;
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream);
Assert.Equal(image.Size, decoded.Size);
}
[Fact]
public void LegacyJpegWritesNonSeekableStream()
{
using Image<Rgba32> image = new(1, 1);
image[0, 0] = new Rgba32(10, 20, 30);
using MemoryStream storage = new();
using NonSeekableStream destination = new(storage);
image.Save(
destination,
new HeifEncoder { CompressionMethod = HeifCompressionMethod.LegacyJpeg });
Assert.NotEqual(0, storage.Length);
storage.Position = 0;
using Image<Rgba32> decoded = Image.Load<Rgba32>(storage);
Assert.Equal(image.Size, decoded.Size);
}
[Fact]
public void LegacyJpegWritesAtCurrentStreamPosition()
{
using Image<Rgba32> image = new(1, 1);
image[0, 0] = new Rgba32(10, 20, 30);
using MemoryStream stream = new();
stream.Write([1, 2, 3, 4]);
long fileStart = stream.Position;
image.Save(
stream,
new HeifEncoder { CompressionMethod = HeifCompressionMethod.LegacyJpeg });
stream.Position = fileStart;
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream);
Assert.Equal(image.Size, decoded.Size);
}
[Theory]
[InlineData(false, true)]
[InlineData(true, false)]
public void LegacyJpegHonorsSkipMetadataForEmbeddedProfiles(bool skipMetadata, bool expectedIccProfile)
{
using Image<Rgb24> image = new(8, 8);
image.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.ProfileRandomArray);
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.LegacyJpeg,
SkipMetadata = skipMetadata
};
image.Save(stream, encoder);
DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve };
stream.Position = 0;
using Image<Rgb24> decoded = Image.Load<Rgb24>(preserveOptions, stream);
Assert.Equal(expectedIccProfile, decoded.Metadata.IccProfile is not null);
if (expectedIccProfile)
{
Assert.Equal(
IccTestDataProfiles.ProfileRandomArray,
Assert.IsType<IccProfile>(decoded.Metadata.IccProfile).ToByteArray());
}
}
[Fact]
public void LegacyJpegIgnoresLosslessOption()
{
using Image<Rgba32> image = new(1, 1);
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.LegacyJpeg,
Lossless = true
};
image.Save(stream, encoder);
stream.Position = 0;
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream);
Assert.Equal(image.Size, decoded.Size);
Assert.Equal(
HeifCompressionMethod.LegacyJpeg,
decoded.Metadata.GetHeifMetadata().CompressionMethod);
}
[Theory]
[InlineData(HeifBitDepth.Bit10)]
[InlineData(HeifBitDepth.Bit12)]
public void LegacyJpegNormalizesHighBitDepthToEightBit(HeifBitDepth bitDepth)
{
using Image<Rgba32> image = new(1, 1);
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.LegacyJpeg,
BitDepth = bitDepth
};
image.Save(stream, encoder);
stream.Position = 0;
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream);
Assert.Equal(HeifBitDepth.Bit8, decoded.Metadata.GetHeifMetadata().BitDepth);
}
[Fact]
public void LegacyJpegEncodesRootFrameFromImageSequence()
{
using Image<Rgba32> image = new(1, 1);
image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0] = new Rgba32(255, 255, 255);
image.Frames.AddFrame(image.Frames.RootFrame);
image.Frames[1].PixelBuffer.DangerousGetRowSpan(0)[0] = new Rgba32(0, 0, 0);
using MemoryStream stream = new();
HeifEncoder encoder = new()
{
CompressionMethod = HeifCompressionMethod.LegacyJpeg,
Quality = 100
};
image.Save(stream, encoder);
stream.Position = 0;
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream);
Assert.Single(decoded.Frames);
Assert.Equal(new Rgba32(255, 255, 255), decoded.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0]);
}
[Fact] [Fact]
public void Av1ImageSequencePreservesSeparateRootFrame() public void Av1ImageSequencePreservesSeparateRootFrame()
{ {
@ -250,7 +107,6 @@ public class HeifEncoderTests
using MemoryStream stream = new(); using MemoryStream stream = new();
HeifEncoder encoder = new() HeifEncoder encoder = new()
{ {
CompressionMethod = HeifCompressionMethod.Av1,
AnimateRootFrame = false, AnimateRootFrame = false,
Lossless = true, Lossless = true,
Effort = 0 Effort = 0
@ -342,11 +198,22 @@ public class HeifEncoderTests
List<HeifItemLink> links = [gridLink]; List<HeifItemLink> links = [gridLink];
GridHeifItemDecoder<Rgba32> decoder = new(items, links, ReadItem); GridHeifItemDecoder<Rgba32> decoder = new(items, links, ReadItem);
Span<byte> descriptor = [0, 0, 1, 1, 0, outputWidth, 0, outputHeight]; Span<byte> descriptor = [0, 0, 1, 1, 0, outputWidth, 0, outputHeight];
using Image<Rgba32> result = decoder.DecodeItemData( using Image<Rgba32> result = new(outputWidth, outputHeight);
decoder.DecodeItemData(
new DecoderOptions { Configuration = Configuration.Default }, new DecoderOptions { Configuration = Configuration.Default },
HeifChromaUpsampling.Auto,
gridItem, gridItem,
descriptor, descriptor,
null, null,
null,
null,
default,
default,
false,
result.Bounds,
default,
result.Frames.RootFrame.PixelBuffer.GetRegion(result.Bounds),
result.Metadata,
TestContext.Current.CancellationToken); TestContext.Current.CancellationToken);
for (int y = 0; y < outputHeight; y++) for (int y = 0; y < outputHeight; y++)
@ -360,15 +227,20 @@ public class HeifEncoderTests
Rgba32 opaqueColor = new(7, 11, 13); Rgba32 opaqueColor = new(7, 11, 13);
using Image<Rgba32> alphaResult = new(outputWidth, outputHeight, opaqueColor); using Image<Rgba32> alphaResult = new(outputWidth, outputHeight, opaqueColor);
decoder.DecodeAlphaItemData( using Av1FrameBuffer<byte> alphaFrame = decoder.DecodeAlphaItemData(
new DecoderOptions { Configuration = Configuration.Default }, new DecoderOptions { Configuration = Configuration.Default },
gridItem, gridItem,
descriptor, descriptor,
alphaResult.Frames.RootFrame, TestContext.Current.CancellationToken);
Av1YuvConverter.ComposeAlpha(
Configuration.Default,
alphaFrame,
alphaResult.Frames.RootFrame.PixelBuffer.GetRegion(alphaResult.Bounds),
alphaResult.Size, alphaResult.Size,
new Rectangle(Point.Empty, alphaResult.Size), new Rectangle(Point.Empty, alphaResult.Size),
false, false,
TestContext.Current.CancellationToken); default);
for (int y = 0; y < outputHeight; y++) for (int y = 0; y < outputHeight; y++)
{ {
@ -469,7 +341,6 @@ public class HeifEncoderTests
using MemoryStream stream = new(); using MemoryStream stream = new();
HeifEncoder encoder = new() HeifEncoder encoder = new()
{ {
CompressionMethod = HeifCompressionMethod.Av1,
Lossless = true, Lossless = true,
ChromaSubsampling = chromaSubsampling, ChromaSubsampling = chromaSubsampling,
Effort = 0 Effort = 0
@ -482,9 +353,40 @@ public class HeifEncoderTests
GetItemPayload(file, 1).ToArray()); GetItemPayload(file, 1).ToArray());
using Av1Decoder firstCellDecoder = new(Configuration.Default); using Av1Decoder firstCellDecoder = new(Configuration.Default);
using Image<Rgb24> firstCell = firstCellDecoder.Decode<Rgb24>(GetItemPayload(file, 2)); using Av1FrameBuffer<byte> firstCellPlanes = firstCellDecoder.DecodeFrameBuffer(GetItemPayload(file, 2), null, null, out _);
using Image<Rgb24> firstCell = new(Configuration.Default, firstCellPlanes.Width, firstCellPlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
firstCellPlanes,
firstCell.Bounds,
firstCell.Frames.RootFrame.PixelBuffer.GetRegion(firstCell.Bounds),
firstCell.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
firstCellPlanes.ColorConfig.ColorRange);
using Av1Decoder secondCellDecoder = new(Configuration.Default); using Av1Decoder secondCellDecoder = new(Configuration.Default);
using Image<Rgb24> secondCell = secondCellDecoder.Decode<Rgb24>(GetItemPayload(file, 3)); using Av1FrameBuffer<byte> secondCellPlanes = secondCellDecoder.DecodeFrameBuffer(GetItemPayload(file, 3), null, null, out _);
using Image<Rgb24> secondCell = new(Configuration.Default, secondCellPlanes.Width, secondCellPlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
secondCellPlanes,
secondCell.Bounds,
secondCell.Frames.RootFrame.PixelBuffer.GetRegion(secondCell.Bounds),
secondCell.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
secondCellPlanes.ColorConfig.ColorRange);
// Odd grid dimensions require full-resolution chroma, including when subsampling was explicitly requested. // Odd grid dimensions require full-resolution chroma, including when subsampling was explicitly requested.
// The conversion must retain the source profile and signal the resolved sampling on every coded cell. // The conversion must retain the source profile and signal the resolved sampling on every coded cell.
@ -526,138 +428,69 @@ public class HeifEncoderTests
Assert.Empty(ImageComparer.Exact.CompareImages(image, decoded)); Assert.Empty(ImageComparer.Exact.CompareImages(image, decoded));
} }
[Fact] [Theory]
public void Av1LosslessRoundTripPreservesColorAndAlpha() [WithFile(TestImages.Png.Ducky, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Bike, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Splash, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Transparency, PixelTypes.Rgba32)]
public void LosslessRgba(TestImageProvider<Rgba32> provider)
{ {
const int width = 8; using Image<Rgba32> image = provider.GetImage();
const int height = 8;
using Image<Rgba32> image = new(width, height);
for (int row = 0; row < height; row++)
{
Span<Rgba32> pixels = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row);
for (int column = 0; column < width; column++)
{
pixels[column] = new Rgba32(
(byte)((column * 31) + row),
(byte)((row * 29) + column),
(byte)((column * 17) + (row * 11)),
(byte)((column * 23) + (row * 7)));
}
}
// Identity-matrix 4:4:4 maps the packed RGB channels directly onto AV1 planes, so codec losslessness // Identity 4:4:4 preserves the source RGB samples without matrix or chroma-subsampling losses.
// can be asserted against the original pixels without a separate color-conversion tolerance.
image.Metadata.CicpProfile = new CicpProfile(1, 13, 0, true); image.Metadata.CicpProfile = new CicpProfile(1, 13, 0, true);
using MemoryStream stream = new();
HeifEncoder encoder = new() HeifEncoder encoder = new()
{ {
CompressionMethod = HeifCompressionMethod.Av1,
Lossless = true, Lossless = true,
Effort = 0 Effort = 0
}; };
image.Save(stream, encoder); string outputFile = image.VerifyEncoder(
byte[] file = stream.ToArray(); provider,
Span<byte> colorPayload = GetItemPayload(file, 1); "avif",
using Av1Decoder colorDecoder = new(Configuration.Default); null,
using Image<Rgba32> colorImage = colorDecoder.Decode<Rgba32>(colorPayload); encoder,
ObuSequenceHeader colorSequenceHeader = Assert.IsType<ObuSequenceHeader>(colorDecoder.SequenceHeader); ImageComparer.Exact,
ObuFrameHeader colorFrameHeader = Assert.IsType<ObuFrameHeader>(colorDecoder.FrameHeader); referenceDecoder: MagickReferenceDecoder.Heif);
Assert.Equal(Av1ColorFormat.Yuv444, colorSequenceHeader.ColorConfig.GetColorFormat());
Assert.Equal(0, colorFrameHeader.QuantizationParameters.BaseQIndex);
Assert.True(colorFrameHeader.CodedLossless);
Assert.True(colorFrameHeader.AllLossless);
Assert.Equal(Av1TransformMode.Only4x4, colorFrameHeader.TransformMode);
Span<byte> alphaPayload = GetItemPayload(file, 2);
using Av1Decoder alphaDecoder = new(Configuration.Default);
using Image<L8> alphaImage = alphaDecoder.Decode<L8>(alphaPayload);
ObuFrameHeader alphaFrameHeader = Assert.IsType<ObuFrameHeader>(alphaDecoder.FrameHeader);
Assert.Equal(0, alphaFrameHeader.QuantizationParameters.BaseQIndex);
Assert.True(alphaFrameHeader.CodedLossless);
stream.Position = 0;
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream);
Assert.Empty(ImageComparer.Exact.CompareImages(image, decoded));
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory); using FileStream stream = File.OpenRead(outputFile);
File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-public-lossless-color.obu"), colorPayload.ToArray()); using Image<Rgba32> reference = MagickReferenceDecoder.Heif.Decode<Rgba32>(DecoderOptions.Default, stream);
File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-public-lossless-alpha.obu"), alphaPayload.ToArray()); reference.DebugSave(provider, extension: "png", encoder: new PngEncoder());
} }
[Theory] [Theory]
[InlineData(HeifBitDepth.Bit10)] [WithFile(TestImages.Tiff.Rgba10BitUnassociatedAlphaBigEndian, PixelTypes.Rgba64, HeifBitDepth.Bit10)]
[InlineData(HeifBitDepth.Bit12)] [WithFile(TestImages.Tiff.Rgba12BitUnassociatedAlphaBigEndian, PixelTypes.Rgba64, HeifBitDepth.Bit12)]
public void Av1LosslessRoundTripPreservesHighBitDepthSourcePixels(HeifBitDepth bitDepth) public void Av1LosslessRoundTripPreservesHighBitDepthSourcePixels(TestImageProvider<Rgba64> provider, HeifBitDepth bitDepth)
{ {
const int width = 8; using Image<Rgba64> image = provider.GetImage();
const int height = 8;
int codedMaximum = (1 << (int)bitDepth) - 1;
using Image<Rgba64> image = new(width, height);
for (int row = 0; row < height; row++)
{
Span<Rgba64> pixels = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row);
for (int column = 0; column < width; column++)
{
int red = ((column * 131) + (row * 37) + 1) & codedMaximum;
int green = ((column * 61) + (row * 173) + 3) & codedMaximum;
int blue = ((column * 211) + (row * 47) + 5) & codedMaximum;
int alpha = ((column * 127) + (row * 89)) & codedMaximum;
pixels[column] = new Rgba64(
ExpandToUShort(red, codedMaximum),
ExpandToUShort(green, codedMaximum),
ExpandToUShort(blue, codedMaximum),
ExpandToUShort(alpha, codedMaximum));
}
}
// Full-range identity 4:4:4 preserves the requested sample lattice, isolating source precision from a // Identity 4:4:4 retains the source's native 10/12-bit RGB sample precision without a color matrix.
// deliberately lossy color matrix or chroma subsampling step.
image.Metadata.CicpProfile = new CicpProfile(1, 13, 0, true); image.Metadata.CicpProfile = new CicpProfile(1, 13, 0, true);
using MemoryStream stream = new();
HeifEncoder encoder = new() HeifEncoder encoder = new()
{ {
CompressionMethod = HeifCompressionMethod.Av1,
BitDepth = bitDepth, BitDepth = bitDepth,
ChromaSubsampling = HeifChromaSubsampling.Yuv444, ChromaSubsampling = HeifChromaSubsampling.Yuv444,
Lossless = true, Lossless = true,
Effort = 0 Effort = 0
}; };
image.Save(stream, encoder); string outputFile = image.VerifyEncoder(
byte[] file = stream.ToArray(); provider,
Span<byte> colorPayload = GetItemPayload(file, 1); "avif",
Span<byte> alphaPayload = GetItemPayload(file, 2); bitDepth,
stream.Position = 0; encoder,
using Image<Rgba64> decoded = Image.Load<Rgba64>(stream); ImageComparer.Exact,
Assert.Empty(ImageComparer.Exact.CompareImages(image, decoded)); referenceDecoder: MagickReferenceDecoder.Heif);
string outputDirectory = Path.Combine( using FileStream stream = File.OpenRead(outputFile);
TestEnvironment.ActualOutputDirectoryFullPath, using Image<Rgba64> reference = MagickReferenceDecoder.Heif.Decode<Rgba64>(DecoderOptions.Default, stream);
"Formats", reference.DebugSave(provider, bitDepth, encoder: new PngEncoder
"Heif", {
"Av1"); BitDepth = PngBitDepth.Bit16
});
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(
Path.Combine(outputDirectory, $"encoder-public-lossless-{(int)bitDepth}b-color.obu"),
colorPayload.ToArray());
File.WriteAllBytes(
Path.Combine(outputDirectory, $"encoder-public-lossless-{(int)bitDepth}b-alpha.obu"),
alphaPayload.ToArray());
} }
private static ushort ExpandToUShort(int sample, int maximum)
=> (ushort)(((sample * (long)ushort.MaxValue) + (maximum / 2)) / maximum);
[Theory] [Theory]
[InlineData((ushort)0, null, (ushort)0)] [InlineData((ushort)0, null, (ushort)0)]
[InlineData((ushort)3, null, (ushort)3)] [InlineData((ushort)3, null, (ushort)3)]
@ -708,7 +541,6 @@ public class HeifEncoderTests
using MemoryStream stream = new(); using MemoryStream stream = new();
HeifEncoder encoder = new() HeifEncoder encoder = new()
{ {
CompressionMethod = HeifCompressionMethod.Av1,
Lossless = true, Lossless = true,
Effort = 0, Effort = 0,
RepeatCount = encoderRepeatCount RepeatCount = encoderRepeatCount
@ -720,8 +552,24 @@ public class HeifEncoderTests
Assert.Equal(1, BinaryPrimitives.ReadUInt16BigEndian(GetMetadataChild(file, Heif4CharCode.Pitm)[12..])); Assert.Equal(1, BinaryPrimitives.ReadUInt16BigEndian(GetMetadataChild(file, Heif4CharCode.Pitm)[12..]));
using (Av1Decoder sampleDecoder = new(Configuration.Default)) using (Av1Decoder sampleDecoder = new(Configuration.Default))
using (Image<Rgba32> decodedSample = sampleDecoder.Decode<Rgba32>(GetItemPayload(file, 1)))
{ {
using Av1FrameBuffer<byte> decodedSamplePlanes = sampleDecoder.DecodeFrameBuffer(GetItemPayload(file, 1), null, null, out _);
using Image<Rgba32> decodedSample = new(Configuration.Default, decodedSamplePlanes.Width, decodedSamplePlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
decodedSamplePlanes,
decodedSample.Bounds,
decodedSample.Frames.RootFrame.PixelBuffer.GetRegion(decodedSample.Bounds),
decodedSample.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
decodedSamplePlanes.ColorConfig.ColorRange);
ObuSequenceHeader sampleHeader = sampleDecoder.SequenceHeader; ObuSequenceHeader sampleHeader = sampleDecoder.SequenceHeader;
Assert.NotNull(sampleHeader); Assert.NotNull(sampleHeader);
Assert.False(sampleHeader.IsStillPicture); Assert.False(sampleHeader.IsStillPicture);
@ -768,7 +616,6 @@ public class HeifEncoderTests
using NonSeekableStream destination = new(storage); using NonSeekableStream destination = new(storage);
HeifEncoder encoder = new() HeifEncoder encoder = new()
{ {
CompressionMethod = HeifCompressionMethod.Av1,
Effort = 0, Effort = 0,
SkipMetadata = true SkipMetadata = true
}; };
@ -795,28 +642,14 @@ public class HeifEncoderTests
public void Av1QualityMapsThroughLibaomQuantizers(int quality, int expectedQIndex) public void Av1QualityMapsThroughLibaomQuantizers(int quality, int expectedQIndex)
=> Assert.Equal(expectedQIndex, HeifEncoderCore.GetAv1QuantizerIndex(quality)); => Assert.Equal(expectedQIndex, HeifEncoderCore.GetAv1QuantizerIndex(quality));
[Fact] [Theory]
public void Av1WritesStillImageWithRequiredBrandsAndProductionPayload() [WithFile(TestImages.Png.Bike, PixelTypes.Rgb24)]
public void Av1WritesStillImageWithRequiredBrandsAndColorDescription(TestImageProvider<Rgb24> provider)
{ {
const int width = 16; using Image<Rgb24> image = provider.GetImage();
const int height = 16;
using Image<Rgb24> image = new(width, height);
for (int row = 0; row < height; row++)
{
Span<Rgb24> pixels = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row);
for (int column = 0; column < width; column++)
{
pixels[column] = new Rgb24(
(byte)(column * 11),
(byte)(row * 13),
(byte)((column + row) * 7));
}
}
using MemoryStream stream = new(); using MemoryStream stream = new();
HeifEncoder encoder = new() HeifEncoder encoder = new()
{ {
CompressionMethod = HeifCompressionMethod.Av1,
Quality = 75, Quality = 75,
Effort = 0 Effort = 0
}; };
@ -832,34 +665,16 @@ public class HeifEncoderTests
Assert.Equal(Heif4CharCode.Mif1, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(fileType[20..])); Assert.Equal(Heif4CharCode.Mif1, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(fileType[20..]));
Assert.Equal(Heif4CharCode.Miaf, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(fileType[24..])); Assert.Equal(Heif4CharCode.Miaf, (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(fileType[24..]));
Span<byte> payload = GetItemPayload(file, 1);
using Av1Decoder payloadDecoder = new(Configuration.Default);
using Image<Rgb24> payloadImage = payloadDecoder.Decode<Rgb24>(payload);
ObuFrameHeader frameHeader = Assert.IsType<ObuFrameHeader>(payloadDecoder.FrameHeader);
Assert.Equal(64, frameHeader.QuantizationParameters.BaseQIndex);
Assert.Equal(image.Size, payloadImage.Size);
stream.Position = 0; stream.Position = 0;
using Image<Rgb24> decoded = Image.Load<Rgb24>(stream); ImageInfo info = Image.Identify(stream);
HeifMetadata metadata = decoded.Metadata.GetHeifMetadata(); HeifMetadata metadata = info.Metadata.GetHeifMetadata();
Assert.Equal(image.Size, decoded.Size); Assert.Equal(image.Size, info.Size);
Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod);
Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth); Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth);
Assert.False(metadata.IsMonochrome); Assert.False(metadata.IsMonochrome);
Assert.False(metadata.HasAlpha); Assert.False(metadata.HasAlpha);
CicpProfile colorProfile = Assert.IsType<CicpProfile>(decoded.Metadata.CicpProfile); CicpProfile colorProfile = Assert.IsType<CicpProfile>(info.Metadata.CicpProfile);
Assert.Equal(CicpMatrixCoefficients.ItuRBt601_7_525, colorProfile.MatrixCoefficients); Assert.Equal(CicpMatrixCoefficients.ItuRBt601_7_525, colorProfile.MatrixCoefficients);
Assert.False(colorProfile.FullRange); Assert.False(colorProfile.FullRange);
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-public-16x16-8b-420.avif"), file);
File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-public-16x16-8b-420.obu"), payload.ToArray());
} }
[Fact] [Fact]
@ -882,7 +697,6 @@ public class HeifEncoderTests
using MemoryStream stream = new(); using MemoryStream stream = new();
HeifEncoder encoder = new() HeifEncoder encoder = new()
{ {
CompressionMethod = HeifCompressionMethod.Av1,
Quality = 75, Quality = 75,
AlphaQuality = 100, AlphaQuality = 100,
Effort = 0 Effort = 0
@ -893,12 +707,44 @@ public class HeifEncoderTests
Span<byte> colorPayload = GetItemPayload(file, 1); Span<byte> colorPayload = GetItemPayload(file, 1);
Span<byte> alphaPayload = GetItemPayload(file, 2); Span<byte> alphaPayload = GetItemPayload(file, 2);
using Av1Decoder colorDecoder = new(Configuration.Default); using Av1Decoder colorDecoder = new(Configuration.Default);
using Image<Rgba32> colorImage = colorDecoder.Decode<Rgba32>(colorPayload); using Av1FrameBuffer<byte> colorImagePlanes = colorDecoder.DecodeFrameBuffer(colorPayload, null, null, out _);
using Image<Rgba32> colorImage = new(Configuration.Default, colorImagePlanes.Width, colorImagePlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
colorImagePlanes,
colorImage.Bounds,
colorImage.Frames.RootFrame.PixelBuffer.GetRegion(colorImage.Bounds),
colorImage.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
colorImagePlanes.ColorConfig.ColorRange);
ObuFrameHeader colorFrameHeader = Assert.IsType<ObuFrameHeader>(colorDecoder.FrameHeader); ObuFrameHeader colorFrameHeader = Assert.IsType<ObuFrameHeader>(colorDecoder.FrameHeader);
Assert.Equal(64, colorFrameHeader.QuantizationParameters.BaseQIndex); Assert.Equal(64, colorFrameHeader.QuantizationParameters.BaseQIndex);
using Av1Decoder alphaDecoder = new(Configuration.Default); using Av1Decoder alphaDecoder = new(Configuration.Default);
using Image<L8> alphaImage = alphaDecoder.Decode<L8>(alphaPayload); using Av1FrameBuffer<byte> alphaImagePlanes = alphaDecoder.DecodeFrameBuffer(alphaPayload, null, null, out _);
using Image<L8> alphaImage = new(Configuration.Default, alphaImagePlanes.Width, alphaImagePlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
alphaImagePlanes,
alphaImage.Bounds,
alphaImage.Frames.RootFrame.PixelBuffer.GetRegion(alphaImage.Bounds),
alphaImage.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
alphaImagePlanes.ColorConfig.ColorRange);
ObuSequenceHeader alphaSequenceHeader = Assert.IsType<ObuSequenceHeader>(alphaDecoder.SequenceHeader); ObuSequenceHeader alphaSequenceHeader = Assert.IsType<ObuSequenceHeader>(alphaDecoder.SequenceHeader);
ObuFrameHeader alphaFrameHeader = Assert.IsType<ObuFrameHeader>(alphaDecoder.FrameHeader); ObuFrameHeader alphaFrameHeader = Assert.IsType<ObuFrameHeader>(alphaDecoder.FrameHeader);
Assert.True(alphaSequenceHeader.ColorConfig.IsMonochrome); Assert.True(alphaSequenceHeader.ColorConfig.IsMonochrome);
@ -910,16 +756,6 @@ public class HeifEncoderTests
Assert.True(metadata.HasAlpha); Assert.True(metadata.HasAlpha);
Assert.InRange(decoded[0, 0].A, (byte)0, (byte)8); Assert.InRange(decoded[0, 0].A, (byte)0, (byte)8);
Assert.InRange(decoded[width - 1, 0].A, (byte)247, byte.MaxValue); Assert.InRange(decoded[width - 1, 0].A, (byte)247, byte.MaxValue);
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-public-alpha-color.obu"), colorPayload.ToArray());
File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-public-alpha-auxiliary.obu"), alphaPayload.ToArray());
} }
[Theory] [Theory]
@ -952,7 +788,6 @@ public class HeifEncoderTests
using MemoryStream stream = new(); using MemoryStream stream = new();
HeifEncoder encoder = new() HeifEncoder encoder = new()
{ {
CompressionMethod = HeifCompressionMethod.Av1,
BitDepth = bitDepth, BitDepth = bitDepth,
ChromaSubsampling = chromaSubsampling, ChromaSubsampling = chromaSubsampling,
Effort = 0 Effort = 0
@ -962,7 +797,23 @@ public class HeifEncoderTests
byte[] file = stream.ToArray(); byte[] file = stream.ToArray();
Span<byte> payload = GetItemPayload(file, 1); Span<byte> payload = GetItemPayload(file, 1);
using Av1Decoder payloadDecoder = new(Configuration.Default); using Av1Decoder payloadDecoder = new(Configuration.Default);
using Image<Rgb48> payloadImage = payloadDecoder.Decode<Rgb48>(payload); using Av1FrameBuffer<byte> payloadImagePlanes = payloadDecoder.DecodeFrameBuffer(payload, null, null, out _);
using Image<Rgb48> payloadImage = new(Configuration.Default, payloadImagePlanes.Width, payloadImagePlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
payloadImagePlanes,
payloadImage.Bounds,
payloadImage.Frames.RootFrame.PixelBuffer.GetRegion(payloadImage.Bounds),
payloadImage.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
payloadImagePlanes.ColorConfig.ColorRange);
ObuSequenceHeader sequenceHeader = Assert.IsType<ObuSequenceHeader>(payloadDecoder.SequenceHeader); ObuSequenceHeader sequenceHeader = Assert.IsType<ObuSequenceHeader>(payloadDecoder.SequenceHeader);
Assert.Equal(expectedAv1BitDepth, sequenceHeader.ColorConfig.BitDepth); Assert.Equal(expectedAv1BitDepth, sequenceHeader.ColorConfig.BitDepth);
Assert.Equal(expectedColorFormat, sequenceHeader.ColorConfig.GetColorFormat()); Assert.Equal(expectedColorFormat, sequenceHeader.ColorConfig.GetColorFormat());
@ -973,17 +824,6 @@ public class HeifEncoderTests
HeifMetadata metadata = decoded.Metadata.GetHeifMetadata(); HeifMetadata metadata = decoded.Metadata.GetHeifMetadata();
Assert.Equal(bitDepth, metadata.BitDepth); Assert.Equal(bitDepth, metadata.BitDepth);
Assert.Equal(chromaSubsampling == HeifChromaSubsampling.Monochrome, metadata.IsMonochrome); Assert.Equal(chromaSubsampling == HeifChromaSubsampling.Monochrome, metadata.IsMonochrome);
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(
Path.Combine(outputDirectory, $"encoder-public-8x8-{(int)bitDepth}b-{chromaSubsampling}.obu"),
payload.ToArray());
} }
[Fact] [Fact]
@ -994,7 +834,6 @@ public class HeifEncoderTests
using MemoryStream stream = new(); using MemoryStream stream = new();
HeifEncoder encoder = new() HeifEncoder encoder = new()
{ {
CompressionMethod = HeifCompressionMethod.Av1,
ChromaSubsampling = HeifChromaSubsampling.Yuv444, ChromaSubsampling = HeifChromaSubsampling.Yuv444,
Effort = 0 Effort = 0
}; };
@ -1003,7 +842,23 @@ public class HeifEncoderTests
byte[] file = stream.ToArray(); byte[] file = stream.ToArray();
Span<byte> payload = GetItemPayload(file, 1); Span<byte> payload = GetItemPayload(file, 1);
using Av1Decoder payloadDecoder = new(Configuration.Default); using Av1Decoder payloadDecoder = new(Configuration.Default);
using Image<Rgb48> payloadImage = payloadDecoder.Decode<Rgb48>(payload); using Av1FrameBuffer<byte> payloadImagePlanes = payloadDecoder.DecodeFrameBuffer(payload, null, null, out _);
using Image<Rgb48> payloadImage = new(Configuration.Default, payloadImagePlanes.Width, payloadImagePlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
payloadImagePlanes,
payloadImage.Bounds,
payloadImage.Frames.RootFrame.PixelBuffer.GetRegion(payloadImage.Bounds),
payloadImage.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
payloadImagePlanes.ColorConfig.ColorRange);
ObuSequenceHeader sequenceHeader = Assert.IsType<ObuSequenceHeader>(payloadDecoder.SequenceHeader); ObuSequenceHeader sequenceHeader = Assert.IsType<ObuSequenceHeader>(payloadDecoder.SequenceHeader);
Assert.Equal(Av1BitDepth.TenBit, sequenceHeader.ColorConfig.BitDepth); Assert.Equal(Av1BitDepth.TenBit, sequenceHeader.ColorConfig.BitDepth);
} }
@ -1062,7 +917,6 @@ public class HeifEncoderTests
using MemoryStream stream = new(); using MemoryStream stream = new();
image.Save(stream, new HeifEncoder image.Save(stream, new HeifEncoder
{ {
CompressionMethod = HeifCompressionMethod.Av1,
BitDepth = bitDepth, BitDepth = bitDepth,
ChromaSubsampling = HeifChromaSubsampling.Yuv444, ChromaSubsampling = HeifChromaSubsampling.Yuv444,
Lossless = true, Lossless = true,
@ -1073,7 +927,23 @@ public class HeifEncoderTests
Assert.Equal(fullRange, profile.FullRange); Assert.Equal(fullRange, profile.FullRange);
byte[] file = stream.ToArray(); byte[] file = stream.ToArray();
using Av1Decoder sampleDecoder = new(Configuration.Default); using Av1Decoder sampleDecoder = new(Configuration.Default);
using Image<Rgb24> sample = sampleDecoder.Decode<Rgb24>(GetItemPayload(file, 1)); using Av1FrameBuffer<byte> samplePlanes = sampleDecoder.DecodeFrameBuffer(GetItemPayload(file, 1), null, null, out _);
using Image<Rgb24> sample = new(Configuration.Default, samplePlanes.Width, samplePlanes.Height);
Av1YuvConverter.ConvertToRgb(
Configuration.Default,
samplePlanes,
sample.Bounds,
sample.Frames.RootFrame.PixelBuffer.GetRegion(sample.Bounds),
sample.Size,
default,
null,
null,
default,
default,
false,
HeifChromaUpsampling.Auto,
samplePlanes.ColorConfig.ColorRange);
ObuSequenceHeader header = Assert.IsType<ObuSequenceHeader>(sampleDecoder.SequenceHeader); ObuSequenceHeader header = Assert.IsType<ObuSequenceHeader>(sampleDecoder.SequenceHeader);
Assert.Equal(ObuMatrixCoefficients.Identity, header.ColorConfig.MatrixCoefficients); Assert.Equal(ObuMatrixCoefficients.Identity, header.ColorConfig.MatrixCoefficients);
Assert.Equal((byte)profile.ColorPrimaries, (byte)header.ColorConfig.ColorPrimaries); Assert.Equal((byte)profile.ColorPrimaries, (byte)header.ColorConfig.ColorPrimaries);
@ -1088,7 +958,7 @@ public class HeifEncoderTests
Assert.Equal(profile.ColorPrimaries, decodedProfile.ColorPrimaries); Assert.Equal(profile.ColorPrimaries, decodedProfile.ColorPrimaries);
Assert.Equal(profile.TransferCharacteristics, decodedProfile.TransferCharacteristics); Assert.Equal(profile.TransferCharacteristics, decodedProfile.TransferCharacteristics);
Assert.Equal(CicpMatrixCoefficients.Identity, decodedProfile.MatrixCoefficients); Assert.Equal(CicpMatrixCoefficients.Identity, decodedProfile.MatrixCoefficients);
Assert.Equal(expectedFullRange, decodedProfile.FullRange); Assert.True(decodedProfile.FullRange);
Assert.Equal(image.Frames.Count, decoded.Frames.Count); Assert.Equal(image.Frames.Count, decoded.Frames.Count);
for (int frameIndex = 0; frameIndex < image.Frames.Count; frameIndex++) for (int frameIndex = 0; frameIndex < image.Frames.Count; frameIndex++)
{ {
@ -1105,39 +975,6 @@ public class HeifEncoderTests
} }
} }
} }
string directory = TestEnvironment.CreateOutputDirectory("Heif", "Av1", nameof(this.Av1PreservesIdentityMatrixColorDescription));
string name = $"{(int)bitDepth}-{fullRange}-{sequence}-{srgb}";
File.WriteAllBytes(Path.Combine(directory, name + ".obu"), GetTopLevelBox(file, Heif4CharCode.Mdat)[8..].ToArray());
using BinaryWriter expectedSamples = new(File.Create(Path.Combine(directory, name + ".expected.yuv")));
int depthScale = 1 << ((int)bitDepth - 8);
int bias = expectedFullRange ? 0 : 16 * depthScale;
int range = expectedFullRange ? (1 << (int)bitDepth) - 1 : 219 * depthScale;
for (int frameIndex = 0; frameIndex < image.Frames.Count; frameIndex++)
{
for (int plane = 0; plane < 3; plane++)
{
for (int y = 0; y < Height; y++)
{
ReadOnlySpan<Rgb24> row = image.Frames[frameIndex].PixelBuffer.DangerousGetRowSpan(y);
for (int x = 0; x < Width; x++)
{
// The independent reference is G, B, R with the luma range on every plane.
// These integer sample expectations do not call the production color converter.
int channel = plane == 0 ? row[x].G : plane == 1 ? row[x].B : row[x].R;
int value = bias + (((channel * range) + 127) / 255);
if (bitDepth == HeifBitDepth.Bit8)
{
expectedSamples.Write((byte)value);
}
else
{
expectedSamples.Write((ushort)value);
}
}
}
}
}
} }
[Theory] [Theory]
@ -1158,7 +995,6 @@ public class HeifEncoderTests
using MemoryStream stream = new(); using MemoryStream stream = new();
HeifEncoder encoder = new() HeifEncoder encoder = new()
{ {
CompressionMethod = HeifCompressionMethod.Av1,
ChromaSubsampling = subsampling, ChromaSubsampling = subsampling,
Effort = 0 Effort = 0
}; };
@ -1169,10 +1005,10 @@ public class HeifEncoderTests
Assert.False(sourceProfile.FullRange); Assert.False(sourceProfile.FullRange);
stream.Position = 0; stream.Position = 0;
using Image<Rgb24> decoded = Image.Load<Rgb24>(stream); ImageInfo encoded = Image.Identify(stream);
CicpProfile decodedProfile = Assert.IsType<CicpProfile>(decoded.Metadata.CicpProfile); CicpProfile encodedProfile = Assert.IsType<CicpProfile>(encoded.Metadata.CicpProfile);
Assert.Equal(CicpMatrixCoefficients.ItuRBt601_7_525, decodedProfile.MatrixCoefficients); Assert.Equal(CicpMatrixCoefficients.ItuRBt601_7_525, encodedProfile.MatrixCoefficients);
Assert.False(decodedProfile.FullRange); Assert.False(encodedProfile.FullRange);
// A fallback must change the actual encoded conversion as well as its metadata. Compare with the // A fallback must change the actual encoded conversion as well as its metadata. Compare with the
// same packed pixels explicitly encoded using that fallback matrix and the requested sampling. // same packed pixels explicitly encoded using that fallback matrix and the requested sampling.
@ -1200,7 +1036,6 @@ public class HeifEncoderTests
using MemoryStream stream = new(); using MemoryStream stream = new();
HeifEncoder encoder = new() HeifEncoder encoder = new()
{ {
CompressionMethod = HeifCompressionMethod.Av1,
Effort = 0 Effort = 0
}; };
@ -1280,7 +1115,6 @@ public class HeifEncoderTests
using MemoryStream stream = new(); using MemoryStream stream = new();
HeifEncoder encoder = new() HeifEncoder encoder = new()
{ {
CompressionMethod = HeifCompressionMethod.Av1,
Effort = 0, Effort = 0,
SkipMetadata = true SkipMetadata = true
}; };
@ -1325,7 +1159,6 @@ public class HeifEncoderTests
using NonSeekableStream destination = new(storage); using NonSeekableStream destination = new(storage);
HeifEncoder encoder = new() HeifEncoder encoder = new()
{ {
CompressionMethod = HeifCompressionMethod.Av1,
Effort = 0 Effort = 0
}; };
@ -1345,7 +1178,6 @@ public class HeifEncoderTests
long fileStart = stream.Position; long fileStart = stream.Position;
HeifEncoder encoder = new() HeifEncoder encoder = new()
{ {
CompressionMethod = HeifCompressionMethod.Av1,
Effort = 0 Effort = 0
}; };
@ -1632,44 +1464,6 @@ public class HeifEncoderTests
Assert.Equal(0x8081, BinaryPrimitives.ReadUInt16BigEndian(propertyBox[(finalEntryOffset + 7)..])); Assert.Equal(0x8081, BinaryPrimitives.ReadUInt16BigEndian(propertyBox[(finalEntryOffset + 7)..]));
} }
[Fact]
public void LegacyJpegEncodingDoesNotMutateSourceHeifMetadata()
{
using Image<Rgba32> image = new(1, 1);
image[0, 0] = new Rgba32(10, 20, 30, 255);
HeifMetadata metadata = image.Metadata.GetHeifMetadata();
metadata.CompressionMethod = HeifCompressionMethod.Av1;
using MemoryStream stream = new();
HeifEncoder encoder = new() { CompressionMethod = HeifCompressionMethod.LegacyJpeg };
image.Save(stream, encoder);
Assert.Same(metadata, image.Metadata.GetHeifMetadata());
Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod);
}
[Theory]
[WithFile(TestImages.Heif.IrvineAvif, PixelTypes.Rgba32, HeifCompressionMethod.LegacyJpeg)]
public static void Encode<TPixel>(TestImageProvider<TPixel> provider, HeifCompressionMethod compressionMethod)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(new MagickReferenceDecoder(HeifFormat.Instance));
using MemoryStream stream = new();
HeifEncoder encoder = new() { CompressionMethod = compressionMethod };
image.Save(stream, encoder);
stream.Position = 0;
ImageInfo imageInfo = Image.Identify(stream);
Assert.Equal(image.Size, imageInfo.Size);
stream.Position = 0;
using Image<TPixel> encodedImage = Image.Load<TPixel>(stream);
HeifMetadata heifMetadata = encodedImage.Metadata.GetHeifMetadata();
ImageComparer.Exact.CompareImages(image, encodedImage);
Assert.Equal(compressionMethod, heifMetadata.CompressionMethod);
}
private static Span<byte> GetItemPayload(Span<byte> file, ushort itemId) private static Span<byte> GetItemPayload(Span<byte> file, ushort itemId)
{ {
int offset = 0; int offset = 0;
@ -1767,12 +1561,33 @@ public class HeifEncoderTests
byte[] descriptor = new byte[8]; byte[] descriptor = new byte[8];
BinaryPrimitives.WriteUInt16BigEndian(descriptor.AsSpan(4), (ushort)width); BinaryPrimitives.WriteUInt16BigEndian(descriptor.AsSpan(4), (ushort)width);
BinaryPrimitives.WriteUInt16BigEndian(descriptor.AsSpan(6), (ushort)height); BinaryPrimitives.WriteUInt16BigEndian(descriptor.AsSpan(6), (ushort)height);
return decoder.DecodeItemData( Image<Rgba32> result = new(width, height);
new DecoderOptions { Configuration = Configuration.Default }, try
gridItem, {
descriptor, decoder.DecodeItemData(
null, new DecoderOptions { Configuration = Configuration.Default },
TestContext.Current.CancellationToken); HeifChromaUpsampling.Auto,
gridItem,
descriptor,
null,
null,
null,
default,
default,
false,
result.Bounds,
default,
result.Frames.RootFrame.PixelBuffer.GetRegion(result.Bounds),
result.Metadata,
TestContext.Current.CancellationToken);
return result;
}
catch
{
result.Dispose();
throw;
}
IMemoryOwner<byte> ReadItem(HeifItem item) IMemoryOwner<byte> ReadItem(HeifItem item)
{ {

4
tests/ImageSharp.Tests/Formats/Heif/HeifMetadataTests.cs

@ -15,7 +15,6 @@ public class HeifMetadataTests
{ {
HeifMetadata metadata = new(); HeifMetadata metadata = new();
Assert.Equal(HeifCompressionMethod.LegacyJpeg, metadata.CompressionMethod);
Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth); Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth);
Assert.False(metadata.IsMonochrome); Assert.False(metadata.IsMonochrome);
Assert.False(metadata.HasAlpha); Assert.False(metadata.HasAlpha);
@ -28,7 +27,6 @@ public class HeifMetadataTests
{ {
HeifMetadata metadata = new() HeifMetadata metadata = new()
{ {
CompressionMethod = HeifCompressionMethod.Av1,
BitDepth = HeifBitDepth.Bit12, BitDepth = HeifBitDepth.Bit12,
IsMonochrome = true, IsMonochrome = true,
HasAlpha = true, HasAlpha = true,
@ -38,7 +36,6 @@ public class HeifMetadataTests
HeifMetadata clone = metadata.DeepClone(); HeifMetadata clone = metadata.DeepClone();
Assert.Equal(metadata.CompressionMethod, clone.CompressionMethod);
Assert.Equal(metadata.BitDepth, clone.BitDepth); Assert.Equal(metadata.BitDepth, clone.BitDepth);
Assert.Equal(metadata.IsMonochrome, clone.IsMonochrome); Assert.Equal(metadata.IsMonochrome, clone.IsMonochrome);
Assert.Equal(metadata.HasAlpha, clone.HasAlpha); Assert.Equal(metadata.HasAlpha, clone.HasAlpha);
@ -111,5 +108,4 @@ public class HeifMetadataTests
Assert.Equal(expectedComponentCount, componentInfo.ComponentCount); Assert.Equal(expectedComponentCount, componentInfo.ComponentCount);
Assert.Equal((int)bitDepth, componentInfo.GetMaximumComponentPrecision()); Assert.Equal((int)bitDepth, componentInfo.GetMaximumComponentPrecision());
} }
} }

5
tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs

@ -96,7 +96,6 @@ public class HeifSequenceParserTests
Assert.Equal(new Size(LibavifAnimationSize, LibavifAnimationSize), info.Size); Assert.Equal(new Size(LibavifAnimationSize, LibavifAnimationSize), info.Size);
Assert.Equal(LibavifAnimationFrameCount, info.FrameCount); Assert.Equal(LibavifAnimationFrameCount, info.FrameCount);
Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod);
Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth); Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth);
Assert.Equal(FinitePlayCount, metadata.RepeatCount); Assert.Equal(FinitePlayCount, metadata.RepeatCount);
Assert.False(metadata.HasAlpha); Assert.False(metadata.HasAlpha);
@ -116,7 +115,6 @@ public class HeifSequenceParserTests
Assert.Equal(new Size(LibavifAnimationSize, LibavifAnimationSize), info.Size); Assert.Equal(new Size(LibavifAnimationSize, LibavifAnimationSize), info.Size);
Assert.Equal(LibavifAnimationFrameCount, info.FrameCount); Assert.Equal(LibavifAnimationFrameCount, info.FrameCount);
Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod);
Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth); Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth);
Assert.Equal(InfinitePlayCount, metadata.RepeatCount); Assert.Equal(InfinitePlayCount, metadata.RepeatCount);
Assert.True(metadata.HasAlpha); Assert.True(metadata.HasAlpha);
@ -138,7 +136,6 @@ public class HeifSequenceParserTests
Assert.Equal(new Size(LibavifKeyframeAnimationSize, LibavifKeyframeAnimationSize), info.Size); Assert.Equal(new Size(LibavifKeyframeAnimationSize, LibavifKeyframeAnimationSize), info.Size);
Assert.Equal(LibavifAnimationFrameCount, info.FrameCount); Assert.Equal(LibavifAnimationFrameCount, info.FrameCount);
Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod);
Assert.Equal(HeifBitDepth.Bit12, metadata.BitDepth); Assert.Equal(HeifBitDepth.Bit12, metadata.BitDepth);
} }
@ -156,7 +153,6 @@ public class HeifSequenceParserTests
Assert.Equal(new Size(SyntheticHeight, SyntheticWidth), info.Size); Assert.Equal(new Size(SyntheticHeight, SyntheticWidth), info.Size);
Assert.Equal(2, info.FrameCount); Assert.Equal(2, info.FrameCount);
Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod);
Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth); Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth);
Assert.Equal(3, metadata.RepeatCount); Assert.Equal(3, metadata.RepeatCount);
Assert.False(metadata.HasAlpha); Assert.False(metadata.HasAlpha);
@ -186,7 +182,6 @@ public class HeifSequenceParserTests
ImageInfo info = Image.Identify(options, stream); ImageInfo info = Image.Identify(options, stream);
HeifMetadata metadata = info.Metadata.GetHeifMetadata(); HeifMetadata metadata = info.Metadata.GetHeifMetadata();
Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod);
Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth); Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth);
Assert.Equal(3, metadata.RepeatCount); Assert.Equal(3, metadata.RepeatCount);
Assert.Null(info.Metadata.CicpProfile); Assert.Null(info.Metadata.CicpProfile);

9
tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs

@ -4,6 +4,7 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using ImageMagick; using ImageMagick;
using ImageMagick.Formats; using ImageMagick.Formats;
using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Exr; using SixLabors.ImageSharp.Formats.Exr;
@ -76,6 +77,14 @@ public class MagickReferenceDecoder : ImageDecoder
List<ImageFrame<TPixel>> framesList = []; List<ImageFrame<TPixel>> framesList = [];
foreach (IMagickImage<ushort> magicFrame in magickImageCollection) foreach (IMagickImage<ushort> magicFrame in magickImageCollection)
{ {
if (this.imageFormat is HeifFormat
&& options.ColorProfileHandling == ColorProfileHandling.Convert
&& magicFrame.GetColorProfile() is not null)
{
// Use the decoder contract's target profile so the comparison isolates conversion behavior.
magicFrame.TransformColorSpace(new ColorProfile(CompactSrgbV4Profile.Profile.ToByteArray()));
}
ImageFrame<TPixel> frame = new(configuration, imageWidth, imageHeight); ImageFrame<TPixel> frame = new(configuration, imageWidth, imageHeight);
framesList.Add(frame); framesList.Add(frame);

4
tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeCdefMatchesReference_Rgba32_libavif-cdef-kodim23-8b.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:19f3e0d5357df5dacf16dc73501e5bbe280f04e0f0e0831bfc8f2511497dccf4 oid sha256:568c8dfeb43b36e9f2df3c7edd8a16bcd8fde52a259e61c04d903b0e0767cc62
size 408944 size 371942

4
tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLoopRestorationMatchesReference_Rgba32_libavif-restoration-kodim23-8b.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:2098b78efb22b27473802e4e9e074d4948eda6d6bb06510069c9497e63f37bf5 oid sha256:3c5ee8ff354f1c48d5746af02d76708e7be76d3947f318f9db5855909a804f9e
size 383051 size 346662

4
tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSuperResolutionMatchesReference_Rgba32_libavif-superres-kodim23-8b.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:cd995bb10a9c10eba89584ea49c6a1c853b0168aa3fac324d3d935d9f985851f oid sha256:c5ed33348681cf5fd780ff38afd21f49651fd8a6256b91c8f31d88a315294100
size 383407 size 345399

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_Irvine_CA.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8524059e28b1f64e6070108794213bfe0e87b654d37a7c405e6918d72fc312f9
size 204802

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_Orange4x4.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3f2cf98bfaacc2ff6a229d433cdc0c7ba068447d25d5cd5ca7fc04ae4f9ea07c
size 352

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc-alpha-exif-xmp.avif/00.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:528eb9a1e6fe6e5060265a2d02bb2f314f11fd4274efd9971f0a66aeaa403087
size 695

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc-alpha-exif-xmp.avif/01.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e9973399a7c4d670532905dfcda7878b9709f438136233974580e822e355688d
size 697

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc-alpha-exif-xmp.avif/02.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d30d25fe8302f64c9764b5cc38f93b68922d45f2fa5c5068d4c2308e5bec16fc
size 716

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc-alpha-exif-xmp.avif/03.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a780bfbfd53e0c14477fe3fffdc7d4e3e571988fc49872c04620555328d86f91
size 697

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc-alpha-exif-xmp.avif/04.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bbc64e3c45c4f02c334a528be2e87a99ac5f854382ae36d7ce2bce0fe9c492e9
size 697

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc-audio.avif/00.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fbf8f4f98d052a9ebc1eeaec5253b545004a8094bd9d70662d269efbd946e333
size 695

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc-audio.avif/01.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0c4f51868abaac1ad9742c6be51b897319a47d3e8f5be06163896ed3ef5ccc56
size 697

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc-audio.avif/02.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6443ae807d43ffa6df3e350358081e3ace6fe79fdf2fabf4dae0e79fc7ac90de
size 696

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc-audio.avif/03.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6a288fa21457152d6e9b543a679f1cd0b62e08b157c06bc70b71ad33271ec297
size 697

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc-audio.avif/04.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:32de0769629d5043bfe3219f596eac3c30cbfefa371e0126edb0a4858981f95d
size 697

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc.avif/00.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5d911da2c28fec56a4c11badf7a04b03846b86ba214ce6aaeebe43d696bb9668
size 695

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc.avif/01.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e0957ec2347edf15744a3914a471ee0412402b8e034e0311d906234e54e4c441
size 697

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc.avif/02.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b6c8c30a7551f7fbcfd290f55dbcdb04f93ec0a72cb194c9c0ae9bfcbc2fd6d2
size 696

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc.avif/03.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ecdf8e090b33738a0d7c5aa484f291f6e9ef8ea8b3467bd63de1a103ca3e40a2
size 697

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_colors-animated-8bpc.avif/04.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5d939fd84c88f09609286cf606257534fd571e5acf36194589c22a7d904e94c6
size 697

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_ducky_romm_icc_alpha.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:176d81c3d9b8fb8760d69f11d0804673e4a55704e4110b5eae163a64c9aee62a
size 34758

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_jpeg444_xnconvert.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4268ff56bddccd15637c35d7108d59dd8d2c30a54024d123e1f41250a571dbd0
size 43380

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_libavif-cdef-kodim23-8b.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:568c8dfeb43b36e9f2df3c7edd8a16bcd8fde52a259e61c04d903b0e0767cc62
size 371942

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_libavif-intrabc-abc-8b-444.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4cd476ce81e54eed777e4218f9b0ad38cc509d60d1c38bb0ba2ccc013d4ee053
size 6462

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_libavif-kodim23-8b.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:97aa2d6df27bb8ba5647d08bd0e3da8f25c2411a7f4cabb2ba490012e218aa2f
size 355642

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_libavif-lossless-circle-8b-444.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a9b2d70a65299c6da81c3b495c54e1eab99c1932e44add57cff2ae00c1f36072
size 834

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_libavif-palette-draw-points-8b.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1148ebf6aa4b0f2d069d5e9b9605f6fb2a315e525f18016cdcae23efdd81da84
size 157

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_libavif-profile-8b-400.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:260a860d1f01e56c0154a31a6f4802fd27b5f507f724d55084f5278b388003c2
size 5294

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_libavif-profile-8b-420.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2b39c31c012ad7e194870fcdd074a83f225dc0dfc40731f0c72fa8847821e0fd
size 9631

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_libavif-profile-8b-422.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:253291bc0bcebde6cc0ef6729c52ec025cba4a7ed046a3231297da7c63df3f4a
size 9748

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_libavif-profile-8b-444.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:96423c89e68f6d4e6a349e0693b1c0b106055654681d51c822ab43fd12b86223
size 9703

3
tests/Images/External/ReferenceOutput/HeifDecoderTests/Decode_Rgba32_libavif-progressive-draw-points-8b.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0758c17dc36e38aee9f4389a335c2bf332ab91e4c79d7b0b22994fddd0fd1605
size 186

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save