Browse Source

Remove unsupported patented codec implementation

pull/2633/head
James Jackson-South 3 days ago
parent
commit
78a74d448a
  1. 1134
      HEIF_IMPLEMENTATION_PLAN.md
  2. 6
      ImageSharp.sln
  3. 252
      THIRD-PARTY-NOTICES.TXT
  4. 2
      src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConversionParameters.cs
  5. 151
      src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb16Converter.LibheifCoefficientOperator.cs
  6. 83
      src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb16Converter.LibheifMonochromeOperator.cs
  7. 293
      src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb16Converter.Operator.cs
  8. 347
      src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb16Converter.Parameters.cs
  9. 114
      src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb16Converter.cs
  10. 125
      src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.LibheifCoefficientOperator.cs
  11. 82
      src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.LibheifMonochromeOperator.cs
  12. 281
      src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.Parameters.cs
  13. 96
      src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.cs
  14. 50
      src/ImageSharp/Formats/Heif/Heif4CharCode.cs
  15. 10
      src/ImageSharp/Formats/Heif/Heif4CharCode.tt
  16. 2
      src/ImageSharp/Formats/Heif/HeifCompressionFactory.cs
  17. 5
      src/ImageSharp/Formats/Heif/HeifCompressionMethod.cs
  18. 36
      src/ImageSharp/Formats/Heif/HeifConstants.cs
  19. 73
      src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
  20. 11
      src/ImageSharp/Formats/Heif/HeifEncoderCore.cs
  21. 2
      src/ImageSharp/Formats/Heif/HeifFileType.cs
  22. 7
      src/ImageSharp/Formats/Heif/HeifItem.cs
  23. 198
      src/ImageSharp/Formats/Heif/HeifSequenceParser.cs
  24. 6
      src/ImageSharp/Formats/Heif/HeifSequenceTrack.cs
  25. 81
      src/ImageSharp/Formats/Heif/Hevc/Color/HevcPlanarSampleBuffer.cs
  26. 179
      src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuvConverter.cs
  27. 230
      src/ImageSharp/Formats/Heif/Hevc/HevcBitReader.cs
  28. 88
      src/ImageSharp/Formats/Heif/Hevc/HevcCabacContext.cs
  29. 319
      src/ImageSharp/Formats/Heif/Hevc/HevcCabacContexts.cs
  30. 396
      src/ImageSharp/Formats/Heif/Hevc/HevcCabacDecoder.cs
  31. 575
      src/ImageSharp/Formats/Heif/Hevc/HevcCabacSyntaxReader.cs
  32. 40
      src/ImageSharp/Formats/Heif/Hevc/HevcChromaSampleLocation.cs
  33. 381
      src/ImageSharp/Formats/Heif/Hevc/HevcCodecConfiguration.cs
  34. 53
      src/ImageSharp/Formats/Heif/Hevc/HevcCodedBlockFlags.cs
  35. 217
      src/ImageSharp/Formats/Heif/Hevc/HevcCodingTreeState.cs
  36. 346
      src/ImageSharp/Formats/Heif/Hevc/HevcCoefficientCodingParameters.cs
  37. 422
      src/ImageSharp/Formats/Heif/Hevc/HevcCoefficientDecoder.cs
  38. 150
      src/ImageSharp/Formats/Heif/Hevc/HevcCoefficientScanOrder.cs
  39. 25
      src/ImageSharp/Formats/Heif/Hevc/HevcCoefficientScanType.cs
  40. 64
      src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingFilter.HorizontalEdgeOperator.cs
  41. 70
      src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingFilter.Operator.cs
  42. 65
      src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingFilter.VerticalEdgeOperator.cs
  43. 470
      src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingFilter.cs
  44. 131
      src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingState.cs
  45. 144
      src/ImageSharp/Formats/Heif/Hevc/HevcImageItemBitstream.cs
  46. 35
      src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictionMode.cs
  47. 394
      src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictionState.cs
  48. 346
      src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictor.AngularOperator.cs
  49. 108
      src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictor.DcOperator.cs
  50. 39
      src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictor.Operator.cs
  51. 271
      src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictor.PlanarOperator.cs
  52. 180
      src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictor.References.cs
  53. 387
      src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictor.cs
  54. 412
      src/ImageSharp/Formats/Heif/Hevc/HevcInverseQuantizer.cs
  55. 49
      src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.DiscreteCosine16Operator.cs
  56. 49
      src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.DiscreteCosine32Operator.cs
  57. 49
      src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.DiscreteCosine4Operator.cs
  58. 49
      src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.DiscreteCosine8Operator.cs
  59. 42
      src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.DiscreteSine4Operator.cs
  60. 547
      src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.Operations.cs
  61. 34
      src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.Operator.cs
  62. 244
      src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.cs
  63. 148
      src/ImageSharp/Formats/Heif/Hevc/HevcNalUnit.cs
  64. 78
      src/ImageSharp/Formats/Heif/Hevc/HevcNalUnitHeader.cs
  65. 186
      src/ImageSharp/Formats/Heif/Hevc/HevcParameterSetSyntax.cs
  66. 235
      src/ImageSharp/Formats/Heif/Hevc/HevcPictureBuffer.cs
  67. 396
      src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Deblocking.cs
  68. 232
      src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Prediction.cs
  69. 250
      src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.SampleAdaptiveOffset.cs
  70. 564
      src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.TransformTree.cs
  71. 409
      src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Traversal.cs
  72. 363
      src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.cs
  73. 558
      src/ImageSharp/Formats/Heif/Hevc/HevcPictureParameterSet.cs
  74. 25
      src/ImageSharp/Formats/Heif/Hevc/HevcPlane.cs
  75. 115
      src/ImageSharp/Formats/Heif/Hevc/HevcProfileTierLevel.cs
  76. 111
      src/ImageSharp/Formats/Heif/Hevc/HevcQuantizationParameters.cs
  77. 235
      src/ImageSharp/Formats/Heif/Hevc/HevcReconstructionState.cs
  78. 25
      src/ImageSharp/Formats/Heif/Hevc/HevcResidualDpcmMode.cs
  79. 32
      src/ImageSharp/Formats/Heif/Hevc/HevcResidualReconstructor.LeftShiftTransformSkipOperator.cs
  80. 47
      src/ImageSharp/Formats/Heif/Hevc/HevcResidualReconstructor.Operator.cs
  81. 35
      src/ImageSharp/Formats/Heif/Hevc/HevcResidualReconstructor.RightShiftTransformSkipOperator.cs
  82. 578
      src/ImageSharp/Formats/Heif/Hevc/HevcResidualReconstructor.cs
  83. 51
      src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetFilter.BandOperator.cs
  84. 68
      src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetFilter.EdgeOperator.cs
  85. 72
      src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetFilter.Operator.cs
  86. 581
      src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetFilter.cs
  87. 464
      src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetParameters.cs
  88. 352
      src/ImageSharp/Formats/Heif/Hevc/HevcScalingList.cs
  89. 573
      src/ImageSharp/Formats/Heif/Hevc/HevcSequenceParameterSet.cs
  90. 188
      src/ImageSharp/Formats/Heif/Hevc/HevcShortTermReferencePictureSet.cs
  91. 494
      src/ImageSharp/Formats/Heif/Hevc/HevcSliceSegmentHeader.cs
  92. 25
      src/ImageSharp/Formats/Heif/Hevc/HevcSliceType.cs
  93. 349
      src/ImageSharp/Formats/Heif/Hevc/HevcSupplementalEnhancementInformation.cs
  94. 196
      src/ImageSharp/Formats/Heif/Hevc/HevcTileLayout.cs
  95. 59
      src/ImageSharp/Formats/Heif/Hevc/HevcTransformComponentGeometry.cs
  96. 158
      src/ImageSharp/Formats/Heif/Hevc/HevcTransformUnitGeometry.cs
  97. 156
      src/ImageSharp/Formats/Heif/Hevc/HevcVideoParameterSet.cs
  98. 247
      src/ImageSharp/Formats/Heif/Hevc/HevcVideoUsabilityInformation.cs
  99. 337
      src/ImageSharp/Formats/Heif/HevcHeifItemDecoder.cs
  100. 4
      src/ImageSharp/Formats/Heif/Readme.md

1134
HEIF_IMPLEMENTATION_PLAN.md

File diff suppressed because it is too large

6
ImageSharp.sln

@ -666,12 +666,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Qoi", "Qoi", "{E801B508-493
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Heif", "Heif", "{BA5D603A-C84C-43E5-B300-8BB886B02936}"
ProjectSection(SolutionItems) = preProject
tests\Images\Input\Heif\dwsample-heic-640.heic = tests\Images\Input\Heif\dwsample-heic-640.heic
tests\Images\Input\Heif\image1.heic = tests\Images\Input\Heif\image1.heic
tests\Images\Input\Heif\image2.heic = tests\Images\Input\Heif\image2.heic
tests\Images\Input\Heif\image3.heic = tests\Images\Input\Heif\image3.heic
tests\Images\Input\Heif\image4.heic = tests\Images\Input\Heif\image4.heic
tests\Images\Input\Heif\IMG-20230508-0053.hif = tests\Images\Input\Heif\IMG-20230508-0053.hif
tests\Images\Input\Heif\Irvine_CA.avif = tests\Images\Input\Heif\Irvine_CA.avif
tests\Images\Input\Heif\jpeg444_xnconvert.avif = tests\Images\Input\Heif\jpeg444_xnconvert.avif
tests\Images\Input\Heif\Orange4x4.avif = tests\Images\Input\Heif\Orange4x4.avif

252
THIRD-PARTY-NOTICES.TXT

@ -172,255 +172,3 @@ Alliance for Open Media Patent License 1.0
2.12. Specification. "Specification" means the specification designated by
the Alliance for Open Media as a Final Deliverable for which this
License was issued.
License notice for the HM HEVC Reference Software
-----
Parts of the HEVC implementation are adapted from the HM HEVC Reference
Software.
https://vcgit.hhi.fraunhofer.de/jvet/HM
The copyright in this software is being made available under the BSD
License, included below. This software may be subject to other third party
and contributor rights, including patent rights, and no such rights are
granted under this license.
Copyright (c) 2010-2026, ITU/ISO/IEC
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the ITU/ISO/IEC nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
License notice for the Ittiam libhevc Codec Library
-----
Parts of the HEVC implementation are adapted from the Android fork of the
Ittiam libhevc Codec Library.
https://android.googlesource.com/platform/external/libhevc/
Copyright (C) 2012 Ittiam Systems Pvt Ltd, Bangalore
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

2
src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConversionParameters.cs

@ -182,7 +182,7 @@ internal readonly struct HeifColorConversionParameters
public float RgbSampleMaximum { get; }
/// <summary>
/// Resolves the H.273 matrix coefficients and sample ranges shared by HEVC and AV1 image items.
/// Resolves the H.273 matrix coefficients and sample ranges used by AV1 image items.
/// </summary>
/// <param name="colorPrimaries">The H.273 color-primary code point.</param>
/// <param name="transferCharacteristics">The H.273 transfer-characteristic code point.</param>

151
src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb16Converter.LibheifCoefficientOperator.cs

@ -1,151 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Components;
/// <content>
/// Provides pinned-libheif high-bit-depth coefficient conversion at the source RGB precision.
/// </content>
internal static partial class HeifYuvToRgb16Converter
{
/// <summary>
/// Implements pinned-libheif coefficient conversion for scalar and SIMD lanes.
/// </summary>
private readonly struct LibheifCoefficientOperator : IHeifYuvToRgb16Operator
{
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Convert(
Vector512<int> y,
Vector512<int> cb,
Vector512<int> cr,
in ConversionParameters parameters,
out Vector512<int> r,
out Vector512<int> g,
out Vector512<int> b)
{
Vector512Parameters values = parameters.SixteenLane;
Vector512<float> luma = (Vector512.ConvertToSingle(y) - values.LumaOffset) * values.LumaScale;
Vector512<float> blueDifference = (Vector512.ConvertToSingle(cb) - values.ChromaMidpoint) * values.ChromaScale;
Vector512<float> redDifference = (Vector512.ConvertToSingle(cr) - values.ChromaMidpoint) * values.ChromaScale;
Vector512<float> half = Vector512.Create(0.5F);
// libheif evaluates these as distinct float32 multiplies and adds; FMA changes some 12-bit results by one.
Vector512<float> redValue = Vector512.Multiply(values.RedCr, redDifference);
redValue = Vector512.Add(luma, redValue);
Vector512<float> greenValue = Vector512.Multiply(values.GreenCb, blueDifference);
greenValue = Vector512.Add(luma, greenValue);
Vector512<float> greenRedValue = Vector512.Multiply(values.GreenCr, redDifference);
greenValue = Vector512.Add(greenValue, greenRedValue);
Vector512<float> blueValue = Vector512.Multiply(values.BlueCb, blueDifference);
blueValue = Vector512.Add(luma, blueValue);
Vector512<int> red = Vector512.ConvertToInt32(Vector512.Truncate(Vector512.Add(redValue, half)));
Vector512<int> green = Vector512.ConvertToInt32(Vector512.Truncate(Vector512.Add(greenValue, half)));
Vector512<int> blue = Vector512.ConvertToInt32(Vector512.Truncate(Vector512.Add(blueValue, half)));
r = Vector512.Clamp(red, default, values.Maximum);
g = Vector512.Clamp(green, default, values.Maximum);
b = Vector512.Clamp(blue, default, values.Maximum);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Convert(
Vector256<int> y,
Vector256<int> cb,
Vector256<int> cr,
in ConversionParameters parameters,
out Vector256<int> r,
out Vector256<int> g,
out Vector256<int> b)
{
Vector256Parameters values = parameters.EightLane;
Vector256<float> luma = (Vector256.ConvertToSingle(y) - values.LumaOffset) * values.LumaScale;
Vector256<float> blueDifference = (Vector256.ConvertToSingle(cb) - values.ChromaMidpoint) * values.ChromaScale;
Vector256<float> redDifference = (Vector256.ConvertToSingle(cr) - values.ChromaMidpoint) * values.ChromaScale;
Vector256<float> half = Vector256.Create(0.5F);
Vector256<float> redValue = Vector256.Multiply(values.RedCr, redDifference);
redValue = Vector256.Add(luma, redValue);
Vector256<float> greenValue = Vector256.Multiply(values.GreenCb, blueDifference);
greenValue = Vector256.Add(luma, greenValue);
Vector256<float> greenRedValue = Vector256.Multiply(values.GreenCr, redDifference);
greenValue = Vector256.Add(greenValue, greenRedValue);
Vector256<float> blueValue = Vector256.Multiply(values.BlueCb, blueDifference);
blueValue = Vector256.Add(luma, blueValue);
Vector256<int> red = Vector256.ConvertToInt32(Vector256.Truncate(Vector256.Add(redValue, half)));
Vector256<int> green = Vector256.ConvertToInt32(Vector256.Truncate(Vector256.Add(greenValue, half)));
Vector256<int> blue = Vector256.ConvertToInt32(Vector256.Truncate(Vector256.Add(blueValue, half)));
r = Vector256.Clamp(red, default, values.Maximum);
g = Vector256.Clamp(green, default, values.Maximum);
b = Vector256.Clamp(blue, default, values.Maximum);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Convert(
Vector128<int> y,
Vector128<int> cb,
Vector128<int> cr,
in ConversionParameters parameters,
out Vector128<int> r,
out Vector128<int> g,
out Vector128<int> b)
{
Vector128Parameters values = parameters.FourLane;
Vector128<float> luma = (Vector128.ConvertToSingle(y) - values.LumaOffset) * values.LumaScale;
Vector128<float> blueDifference = (Vector128.ConvertToSingle(cb) - values.ChromaMidpoint) * values.ChromaScale;
Vector128<float> redDifference = (Vector128.ConvertToSingle(cr) - values.ChromaMidpoint) * values.ChromaScale;
Vector128<float> half = Vector128.Create(0.5F);
Vector128<float> redValue = Vector128.Multiply(values.RedCr, redDifference);
redValue = Vector128.Add(luma, redValue);
Vector128<float> greenValue = Vector128.Multiply(values.GreenCb, blueDifference);
greenValue = Vector128.Add(luma, greenValue);
Vector128<float> greenRedValue = Vector128.Multiply(values.GreenCr, redDifference);
greenValue = Vector128.Add(greenValue, greenRedValue);
Vector128<float> blueValue = Vector128.Multiply(values.BlueCb, blueDifference);
blueValue = Vector128.Add(luma, blueValue);
Vector128<int> red = Vector128.ConvertToInt32(Vector128.Truncate(Vector128.Add(redValue, half)));
Vector128<int> green = Vector128.ConvertToInt32(Vector128.Truncate(Vector128.Add(greenValue, half)));
Vector128<int> blue = Vector128.ConvertToInt32(Vector128.Truncate(Vector128.Add(blueValue, half)));
r = Vector128.Clamp(red, default, values.Maximum);
g = Vector128.Clamp(green, default, values.Maximum);
b = Vector128.Clamp(blue, default, values.Maximum);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Convert(
ushort y,
ushort cb,
ushort cr,
in ConversionParameters parameters,
out int r,
out int g,
out int b)
{
ScalarParameters values = parameters.Scalar;
float luma = (y - values.LumaOffset) * values.LumaScale;
float blueDifference = (cb - values.ChromaMidpoint) * values.ChromaScale;
float redDifference = (cr - values.ChromaMidpoint) * values.ChromaScale;
// Keep each assignment separate so the JIT cannot fuse the reference float32 operations.
float redValue = values.RedCr * redDifference;
redValue = luma + redValue;
float greenValue = values.GreenCb * blueDifference;
greenValue = luma + greenValue;
float greenRedValue = values.GreenCr * redDifference;
greenValue += greenRedValue;
float blueValue = values.BlueCb * blueDifference;
blueValue = luma + blueValue;
r = Numerics.Clamp((int)(redValue + 0.5F), 0, values.Maximum);
g = Numerics.Clamp((int)(greenValue + 0.5F), 0, values.Maximum);
b = Numerics.Clamp((int)(blueValue + 0.5F), 0, values.Maximum);
}
}
}

83
src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb16Converter.LibheifMonochromeOperator.cs

@ -1,83 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Components;
/// <content>
/// Provides pinned-libheif high-bit-depth monochrome presentation without luma-range expansion.
/// </content>
internal static partial class HeifYuvToRgb16Converter
{
/// <summary>
/// Copies source-precision luma into each source-precision RGB component.
/// </summary>
private readonly struct LibheifMonochromeOperator : IHeifYuvToRgb16Operator
{
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Convert(
Vector512<int> y,
Vector512<int> cb,
Vector512<int> cr,
in ConversionParameters parameters,
out Vector512<int> r,
out Vector512<int> g,
out Vector512<int> b)
{
r = y;
g = y;
b = y;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Convert(
Vector256<int> y,
Vector256<int> cb,
Vector256<int> cr,
in ConversionParameters parameters,
out Vector256<int> r,
out Vector256<int> g,
out Vector256<int> b)
{
r = y;
g = y;
b = y;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Convert(
Vector128<int> y,
Vector128<int> cb,
Vector128<int> cr,
in ConversionParameters parameters,
out Vector128<int> r,
out Vector128<int> g,
out Vector128<int> b)
{
r = y;
g = y;
b = y;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Convert(
ushort y,
ushort cb,
ushort cr,
in ConversionParameters parameters,
out int r,
out int g,
out int b)
{
r = y;
g = y;
b = y;
}
}
}

293
src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb16Converter.Operator.cs

@ -1,293 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp.Formats.Heif.Components;
/// <content>
/// Defines closed high-bit-depth color operators and nearest-sample row traversal.
/// </content>
internal static partial class HeifYuvToRgb16Converter
{
/// <summary>
/// Defines source-precision RGB arithmetic for scalar and SIMD lanes.
/// </summary>
private interface IHeifYuvToRgb16Operator
{
/// <summary>
/// Converts sixteen YCbCr samples to source-precision RGB lanes.
/// </summary>
/// <param name="y">The luma lanes.</param>
/// <param name="cb">The blue-difference lanes.</param>
/// <param name="cr">The red-difference lanes.</param>
/// <param name="parameters">The image conversion parameters.</param>
/// <param name="r">The converted red lanes.</param>
/// <param name="g">The converted green lanes.</param>
/// <param name="b">The converted blue lanes.</param>
public static abstract void Convert(
Vector512<int> y,
Vector512<int> cb,
Vector512<int> cr,
in ConversionParameters parameters,
out Vector512<int> r,
out Vector512<int> g,
out Vector512<int> b);
/// <summary>
/// Converts eight YCbCr samples to source-precision RGB lanes.
/// </summary>
/// <param name="y">The luma lanes.</param>
/// <param name="cb">The blue-difference lanes.</param>
/// <param name="cr">The red-difference lanes.</param>
/// <param name="parameters">The image conversion parameters.</param>
/// <param name="r">The converted red lanes.</param>
/// <param name="g">The converted green lanes.</param>
/// <param name="b">The converted blue lanes.</param>
public static abstract void Convert(
Vector256<int> y,
Vector256<int> cb,
Vector256<int> cr,
in ConversionParameters parameters,
out Vector256<int> r,
out Vector256<int> g,
out Vector256<int> b);
/// <summary>
/// Converts four YCbCr samples to source-precision RGB lanes.
/// </summary>
/// <param name="y">The luma lanes.</param>
/// <param name="cb">The blue-difference lanes.</param>
/// <param name="cr">The red-difference lanes.</param>
/// <param name="parameters">The image conversion parameters.</param>
/// <param name="r">The converted red lanes.</param>
/// <param name="g">The converted green lanes.</param>
/// <param name="b">The converted blue lanes.</param>
public static abstract void Convert(
Vector128<int> y,
Vector128<int> cb,
Vector128<int> cr,
in ConversionParameters parameters,
out Vector128<int> r,
out Vector128<int> g,
out Vector128<int> b);
/// <summary>
/// Converts one YCbCr sample to source-precision RGB.
/// </summary>
/// <param name="y">The luma sample.</param>
/// <param name="cb">The blue-difference sample.</param>
/// <param name="cr">The red-difference sample.</param>
/// <param name="parameters">The image conversion parameters.</param>
/// <param name="r">The converted red sample.</param>
/// <param name="g">The converted green sample.</param>
/// <param name="b">The converted blue sample.</param>
public static abstract void Convert(
ushort y,
ushort cb,
ushort cr,
in ConversionParameters parameters,
out int r,
out int g,
out int b);
}
/// <summary>
/// Converts one luma row and its nearest native chroma row to planar 16-bit RGB storage.
/// </summary>
/// <typeparam name="TOperator">The source-precision color arithmetic selected for the row.</typeparam>
/// <param name="luma">The full-resolution luma samples.</param>
/// <param name="chromaBlue">The native blue-difference samples.</param>
/// <param name="chromaRed">The native red-difference samples.</param>
/// <param name="red">The destination red samples.</param>
/// <param name="green">The destination green samples.</param>
/// <param name="blue">The destination blue samples.</param>
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param>
/// <param name="parameters">The image conversion parameters.</param>
private static void ConvertRow<TOperator>(
ReadOnlySpan<ushort> luma,
ReadOnlySpan<ushort> chromaBlue,
ReadOnlySpan<ushort> chromaRed,
Span<ushort> red,
Span<ushort> green,
Span<ushort> blue,
int subsamplingX,
in ConversionParameters parameters)
where TOperator : struct, IHeifYuvToRgb16Operator
{
ref ushort lumaBase = ref MemoryMarshal.GetReference(luma);
ref ushort chromaBlueBase = ref MemoryMarshal.GetReference(chromaBlue);
ref ushort chromaRedBase = ref MemoryMarshal.GetReference(chromaRed);
ref ushort redBase = ref MemoryMarshal.GetReference(red);
ref ushort greenBase = ref MemoryMarshal.GetReference(green);
ref ushort blueBase = ref MemoryMarshal.GetReference(blue);
int outputLeftShift = parameters.Scalar.OutputLeftShift;
int x = 0;
// Each operator produces code values at the source precision. The traversal then left-aligns those values in
// UInt16 storage, matching libheif's high-bit-depth RGB output without discarding low source bits.
if (Vector512.IsHardwareAccelerated)
{
int oneVectorFromEnd = luma.Length - Vector512<int>.Count;
for (; x <= oneVectorFromEnd; x += Vector512<int>.Count)
{
Vector512<int> y = LoadVector512(ref Unsafe.Add(ref lumaBase, x));
Vector512<int> cb = subsamplingX == 0
? LoadVector512(ref Unsafe.Add(ref chromaBlueBase, x))
: LoadRepeatedVector512(ref Unsafe.Add(ref chromaBlueBase, x >> 1));
Vector512<int> cr = subsamplingX == 0
? LoadVector512(ref Unsafe.Add(ref chromaRedBase, x))
: LoadRepeatedVector512(ref Unsafe.Add(ref chromaRedBase, x >> 1));
TOperator.Convert(y, cb, cr, in parameters, out Vector512<int> r, out Vector512<int> g, out Vector512<int> b);
HeifUShortSampleConverter.Store(r << outputLeftShift, ref Unsafe.Add(ref redBase, x));
HeifUShortSampleConverter.Store(g << outputLeftShift, ref Unsafe.Add(ref greenBase, x));
HeifUShortSampleConverter.Store(b << outputLeftShift, ref Unsafe.Add(ref blueBase, x));
}
}
if (Vector256.IsHardwareAccelerated)
{
int oneVectorFromEnd = luma.Length - Vector256<int>.Count;
for (; x <= oneVectorFromEnd; x += Vector256<int>.Count)
{
Vector256<int> y = LoadVector256(ref Unsafe.Add(ref lumaBase, x));
Vector256<int> cb = subsamplingX == 0
? LoadVector256(ref Unsafe.Add(ref chromaBlueBase, x))
: LoadRepeatedVector256(ref Unsafe.Add(ref chromaBlueBase, x >> 1));
Vector256<int> cr = subsamplingX == 0
? LoadVector256(ref Unsafe.Add(ref chromaRedBase, x))
: LoadRepeatedVector256(ref Unsafe.Add(ref chromaRedBase, x >> 1));
TOperator.Convert(y, cb, cr, in parameters, out Vector256<int> r, out Vector256<int> g, out Vector256<int> b);
HeifUShortSampleConverter.Store(r << outputLeftShift, ref Unsafe.Add(ref redBase, x));
HeifUShortSampleConverter.Store(g << outputLeftShift, ref Unsafe.Add(ref greenBase, x));
HeifUShortSampleConverter.Store(b << outputLeftShift, ref Unsafe.Add(ref blueBase, x));
}
}
if (Vector128.IsHardwareAccelerated)
{
int oneVectorFromEnd = luma.Length - Vector128<int>.Count;
for (; x <= oneVectorFromEnd; x += Vector128<int>.Count)
{
Vector128<int> y = LoadVector128(ref Unsafe.Add(ref lumaBase, x));
Vector128<int> cb = subsamplingX == 0
? LoadVector128(ref Unsafe.Add(ref chromaBlueBase, x))
: LoadRepeatedVector128(ref Unsafe.Add(ref chromaBlueBase, x >> 1));
Vector128<int> cr = subsamplingX == 0
? LoadVector128(ref Unsafe.Add(ref chromaRedBase, x))
: LoadRepeatedVector128(ref Unsafe.Add(ref chromaRedBase, x >> 1));
TOperator.Convert(y, cb, cr, in parameters, out Vector128<int> r, out Vector128<int> g, out Vector128<int> b);
HeifUShortSampleConverter.Store(r << outputLeftShift, ref Unsafe.Add(ref redBase, x));
HeifUShortSampleConverter.Store(g << outputLeftShift, ref Unsafe.Add(ref greenBase, x));
HeifUShortSampleConverter.Store(b << outputLeftShift, ref Unsafe.Add(ref blueBase, x));
}
}
for (; x < luma.Length; x++)
{
TOperator.Convert(
Unsafe.Add(ref lumaBase, x),
Unsafe.Add(ref chromaBlueBase, x >> subsamplingX),
Unsafe.Add(ref chromaRedBase, x >> subsamplingX),
in parameters,
out int r,
out int g,
out int b);
Unsafe.Add(ref redBase, x) = (ushort)(r << outputLeftShift);
Unsafe.Add(ref greenBase, x) = (ushort)(g << outputLeftShift);
Unsafe.Add(ref blueBase, x) = (ushort)(b << outputLeftShift);
}
}
/// <summary>
/// Loads sixteen native samples as signed 32-bit SIMD lanes.
/// </summary>
/// <param name="source">The first native sample.</param>
/// <returns>The widened sample lanes.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<int> LoadVector512(ref ushort source)
{
(Vector256<uint> lower, Vector256<uint> upper) = Vector256.Widen(Vector256.LoadUnsafe(ref source));
return Vector512.Create(lower, upper).AsInt32();
}
/// <summary>
/// Loads eight native samples as signed 32-bit SIMD lanes.
/// </summary>
/// <param name="source">The first native sample.</param>
/// <returns>The widened sample lanes.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<int> LoadVector256(ref ushort source)
{
Vector128<ushort> samples = Vector128.LoadUnsafe(ref source);
return Vector256.Create(Vector128.WidenLower(samples), Vector128.WidenUpper(samples)).AsInt32();
}
/// <summary>
/// Loads four native samples as signed 32-bit SIMD lanes.
/// </summary>
/// <param name="source">The first native sample.</param>
/// <returns>The widened sample lanes.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<int> LoadVector128(ref ushort source)
{
ulong packed = Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<ushort, byte>(ref source));
return Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsUInt16()).AsInt32();
}
/// <summary>
/// Loads eight chroma samples and repeats each sample into two of sixteen 32-bit SIMD lanes.
/// </summary>
/// <param name="source">The first native chroma sample.</param>
/// <returns>The horizontally replicated chroma lanes.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<int> LoadRepeatedVector512(ref ushort source)
{
Vector128<ushort> samples = Vector128.LoadUnsafe(ref source);
Vector128<ushort> lower = Vector128_.UnpackLow(samples.AsInt16(), samples.AsInt16()).AsUInt16();
Vector128<ushort> upper = Vector128_.UnpackHigh(samples.AsInt16(), samples.AsInt16()).AsUInt16();
(Vector256<uint> widenedLower, Vector256<uint> widenedUpper) = Vector256.Widen(Vector256.Create(lower, upper));
return Vector512.Create(widenedLower, widenedUpper).AsInt32();
}
/// <summary>
/// Loads four chroma samples and repeats each sample into two of eight 32-bit SIMD lanes.
/// </summary>
/// <param name="source">The first native chroma sample.</param>
/// <returns>The horizontally replicated chroma lanes.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<int> LoadRepeatedVector256(ref ushort source)
{
ulong packed = Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<ushort, byte>(ref source));
Vector128<ushort> samples = Vector128.CreateScalarUnsafe(packed).AsUInt16();
Vector128<ushort> repeated = Vector128_.UnpackLow(samples.AsInt16(), samples.AsInt16()).AsUInt16();
return Vector256.Create(Vector128.WidenLower(repeated), Vector128.WidenUpper(repeated)).AsInt32();
}
/// <summary>
/// Loads two chroma samples and repeats each sample into two of four 32-bit SIMD lanes.
/// </summary>
/// <param name="source">The first native chroma sample.</param>
/// <returns>The horizontally replicated chroma lanes.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<int> LoadRepeatedVector128(ref ushort source)
{
uint packed = Unsafe.ReadUnaligned<uint>(ref Unsafe.As<ushort, byte>(ref source));
Vector128<ushort> samples = Vector128.CreateScalarUnsafe(packed).AsUInt16();
Vector128<ushort> repeated = Vector128_.UnpackLow(samples.AsInt16(), samples.AsInt16()).AsUInt16();
return Vector128.WidenLower(repeated).AsInt32();
}
}

347
src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb16Converter.Parameters.cs

@ -1,347 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
namespace SixLabors.ImageSharp.Formats.Heif.Components;
/// <content>
/// Provides scalar and SIMD parameter storage for high-bit-depth pinned-libheif conversion.
/// </content>
internal static partial class HeifYuvToRgb16Converter
{
/// <summary>
/// Stores every scalar and SIMD coefficient representation resolved once for an image.
/// </summary>
private readonly struct ConversionParameters
{
/// <summary>
/// Initializes a new instance of the <see cref="ConversionParameters"/> struct.
/// </summary>
/// <param name="parameters">The resolved H.273 matrix and range values.</param>
/// <param name="bitDepth">The common source component precision.</param>
public ConversionParameters(in HeifColorConversionParameters parameters, int bitDepth)
{
ScalarParameters scalar = new(in parameters, bitDepth);
this.Scalar = scalar;
this.SixteenLane = new(in scalar);
this.EightLane = new(in scalar);
this.FourLane = new(in scalar);
}
/// <summary>
/// Gets the scalar conversion parameters.
/// </summary>
public ScalarParameters Scalar { get; }
/// <summary>
/// Gets the sixteen-lane conversion parameters.
/// </summary>
public Vector512Parameters SixteenLane { get; }
/// <summary>
/// Gets the eight-lane conversion parameters.
/// </summary>
public Vector256Parameters EightLane { get; }
/// <summary>
/// Gets the four-lane conversion parameters.
/// </summary>
public Vector128Parameters FourLane { get; }
}
/// <summary>
/// Stores the scalar arithmetic and output scaling used by pinned libheif.
/// </summary>
private readonly struct ScalarParameters
{
/// <summary>
/// Initializes a new instance of the <see cref="ScalarParameters"/> struct.
/// </summary>
/// <param name="parameters">The resolved H.273 matrix and range values.</param>
/// <param name="bitDepth">The common source component precision.</param>
public ScalarParameters(in HeifColorConversionParameters parameters, int bitDepth)
{
this.LumaOffset = parameters.IsFullRange ? 0F : parameters.LumaBias;
this.LumaScale = parameters.IsFullRange ? 1F : 1.1689F;
this.ChromaMidpoint = parameters.ChromaBias;
this.ChromaScale = parameters.IsFullRange ? 1F : 1.1429F;
if (parameters.MatrixCoefficients == CicpMatrixCoefficients.Unspecified)
{
// libheif falls back to these literal Rec.601 coefficients when no matrix is signaled. Deriving them
// from Kr and Kb produces different float32 values and can move high-bit-depth green by one code value.
this.RedCr = 1.402F;
this.GreenCb = -0.344136F;
this.GreenCr = -0.714136F;
this.BlueCb = 1.772F;
}
else
{
float kr = parameters.Kr;
float kb = parameters.Kb;
this.RedCr = 2F * (-kr + 1F);
this.GreenCb = 2F * kb * (-kb + 1F) / (kb + kr - 1F);
this.GreenCr = 2F * kr * (-kr + 1F) / (kb + kr - 1F);
this.BlueCb = 2F * (-kb + 1F);
}
this.Maximum = (1 << bitDepth) - 1;
this.OutputLeftShift = 16 - bitDepth;
}
/// <summary>
/// Gets the luma code-value offset removed before limited-range expansion.
/// </summary>
public float LumaOffset { get; }
/// <summary>
/// Gets the luma range-expansion factor.
/// </summary>
public float LumaScale { get; }
/// <summary>
/// Gets the neutral chroma code value.
/// </summary>
public float ChromaMidpoint { get; }
/// <summary>
/// Gets the chroma range-expansion factor.
/// </summary>
public float ChromaScale { get; }
/// <summary>
/// Gets the red contribution from Cr.
/// </summary>
public float RedCr { get; }
/// <summary>
/// Gets the green contribution from Cb.
/// </summary>
public float GreenCb { get; }
/// <summary>
/// Gets the green contribution from Cr.
/// </summary>
public float GreenCr { get; }
/// <summary>
/// Gets the blue contribution from Cb.
/// </summary>
public float BlueCb { get; }
/// <summary>
/// Gets the largest source-precision RGB code value.
/// </summary>
public int Maximum { get; }
/// <summary>
/// Gets the left shift mapping source-precision RGB into 16-bit pixel storage.
/// </summary>
public int OutputLeftShift { get; }
}
/// <summary>
/// Broadcasts pinned-libheif coefficients for sixteen-lane conversion.
/// </summary>
private readonly struct Vector512Parameters
{
/// <summary>
/// Initializes a new instance of the <see cref="Vector512Parameters"/> struct.
/// </summary>
/// <param name="parameters">The scalar pinned-libheif coefficients.</param>
public Vector512Parameters(in ScalarParameters parameters)
{
this.LumaOffset = Vector512.Create(parameters.LumaOffset);
this.LumaScale = Vector512.Create(parameters.LumaScale);
this.ChromaMidpoint = Vector512.Create(parameters.ChromaMidpoint);
this.ChromaScale = Vector512.Create(parameters.ChromaScale);
this.RedCr = Vector512.Create(parameters.RedCr);
this.GreenCb = Vector512.Create(parameters.GreenCb);
this.GreenCr = Vector512.Create(parameters.GreenCr);
this.BlueCb = Vector512.Create(parameters.BlueCb);
this.Maximum = Vector512.Create(parameters.Maximum);
}
/// <summary>
/// Gets the luma offset lanes.
/// </summary>
public Vector512<float> LumaOffset { get; }
/// <summary>
/// Gets the luma scale lanes.
/// </summary>
public Vector512<float> LumaScale { get; }
/// <summary>
/// Gets the chroma-midpoint lanes.
/// </summary>
public Vector512<float> ChromaMidpoint { get; }
/// <summary>
/// Gets the chroma-scale lanes.
/// </summary>
public Vector512<float> ChromaScale { get; }
/// <summary>
/// Gets the red Cr coefficient lanes.
/// </summary>
public Vector512<float> RedCr { get; }
/// <summary>
/// Gets the green Cb coefficient lanes.
/// </summary>
public Vector512<float> GreenCb { get; }
/// <summary>
/// Gets the green Cr coefficient lanes.
/// </summary>
public Vector512<float> GreenCr { get; }
/// <summary>
/// Gets the blue Cb coefficient lanes.
/// </summary>
public Vector512<float> BlueCb { get; }
/// <summary>
/// Gets the maximum source-precision RGB lanes.
/// </summary>
public Vector512<int> Maximum { get; }
}
/// <summary>
/// Broadcasts pinned-libheif coefficients for eight-lane conversion.
/// </summary>
private readonly struct Vector256Parameters
{
/// <summary>
/// Initializes a new instance of the <see cref="Vector256Parameters"/> struct.
/// </summary>
/// <param name="parameters">The scalar pinned-libheif coefficients.</param>
public Vector256Parameters(in ScalarParameters parameters)
{
this.LumaOffset = Vector256.Create(parameters.LumaOffset);
this.LumaScale = Vector256.Create(parameters.LumaScale);
this.ChromaMidpoint = Vector256.Create(parameters.ChromaMidpoint);
this.ChromaScale = Vector256.Create(parameters.ChromaScale);
this.RedCr = Vector256.Create(parameters.RedCr);
this.GreenCb = Vector256.Create(parameters.GreenCb);
this.GreenCr = Vector256.Create(parameters.GreenCr);
this.BlueCb = Vector256.Create(parameters.BlueCb);
this.Maximum = Vector256.Create(parameters.Maximum);
}
/// <summary>
/// Gets the luma offset lanes.
/// </summary>
public Vector256<float> LumaOffset { get; }
/// <summary>
/// Gets the luma scale lanes.
/// </summary>
public Vector256<float> LumaScale { get; }
/// <summary>
/// Gets the chroma-midpoint lanes.
/// </summary>
public Vector256<float> ChromaMidpoint { get; }
/// <summary>
/// Gets the chroma-scale lanes.
/// </summary>
public Vector256<float> ChromaScale { get; }
/// <summary>
/// Gets the red Cr coefficient lanes.
/// </summary>
public Vector256<float> RedCr { get; }
/// <summary>
/// Gets the green Cb coefficient lanes.
/// </summary>
public Vector256<float> GreenCb { get; }
/// <summary>
/// Gets the green Cr coefficient lanes.
/// </summary>
public Vector256<float> GreenCr { get; }
/// <summary>
/// Gets the blue Cb coefficient lanes.
/// </summary>
public Vector256<float> BlueCb { get; }
/// <summary>
/// Gets the maximum source-precision RGB lanes.
/// </summary>
public Vector256<int> Maximum { get; }
}
/// <summary>
/// Broadcasts pinned-libheif coefficients for four-lane conversion.
/// </summary>
private readonly struct Vector128Parameters
{
/// <summary>
/// Initializes a new instance of the <see cref="Vector128Parameters"/> struct.
/// </summary>
/// <param name="parameters">The scalar pinned-libheif coefficients.</param>
public Vector128Parameters(in ScalarParameters parameters)
{
this.LumaOffset = Vector128.Create(parameters.LumaOffset);
this.LumaScale = Vector128.Create(parameters.LumaScale);
this.ChromaMidpoint = Vector128.Create(parameters.ChromaMidpoint);
this.ChromaScale = Vector128.Create(parameters.ChromaScale);
this.RedCr = Vector128.Create(parameters.RedCr);
this.GreenCb = Vector128.Create(parameters.GreenCb);
this.GreenCr = Vector128.Create(parameters.GreenCr);
this.BlueCb = Vector128.Create(parameters.BlueCb);
this.Maximum = Vector128.Create(parameters.Maximum);
}
/// <summary>
/// Gets the luma offset lanes.
/// </summary>
public Vector128<float> LumaOffset { get; }
/// <summary>
/// Gets the luma scale lanes.
/// </summary>
public Vector128<float> LumaScale { get; }
/// <summary>
/// Gets the chroma-midpoint lanes.
/// </summary>
public Vector128<float> ChromaMidpoint { get; }
/// <summary>
/// Gets the chroma-scale lanes.
/// </summary>
public Vector128<float> ChromaScale { get; }
/// <summary>
/// Gets the red Cr coefficient lanes.
/// </summary>
public Vector128<float> RedCr { get; }
/// <summary>
/// Gets the green Cb coefficient lanes.
/// </summary>
public Vector128<float> GreenCb { get; }
/// <summary>
/// Gets the green Cr coefficient lanes.
/// </summary>
public Vector128<float> GreenCr { get; }
/// <summary>
/// Gets the blue Cb coefficient lanes.
/// </summary>
public Vector128<float> BlueCb { get; }
/// <summary>
/// Gets the maximum source-precision RGB lanes.
/// </summary>
public Vector128<int> Maximum { get; }
}
}

114
src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb16Converter.cs

@ -1,114 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Heif.Components;
/// <summary>
/// Converts high-bit-depth HEIF YUV planes to packed pixels through opaque 16-bit RGB.
/// </summary>
internal static partial class HeifYuvToRgb16Converter
{
/// <summary>
/// Determines whether the pinned libheif-compatible high-bit-depth conversion supports the supplied planes.
/// </summary>
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param>
/// <param name="subsamplingY">The vertical chroma subsampling shift.</param>
/// <param name="lumaBitDepth">The luma sample precision in bits.</param>
/// <param name="chromaBitDepth">The chroma sample precision in bits.</param>
/// <param name="isMonochrome">Whether the image contains only luma samples.</param>
/// <param name="mode">The resolved H.273 conversion operation.</param>
/// <returns><see langword="true"/> when the planes can use this converter; otherwise, <see langword="false"/>.</returns>
public static bool SupportsLibheifConversion(
int subsamplingX,
int subsamplingY,
int lumaBitDepth,
int chromaBitDepth,
bool isMonochrome,
HeifColorConversionMode mode)
=> (isMonochrome || (subsamplingX is 0 or 1 && subsamplingY is 0 or 1))
&& lumaBitDepth is > 8 and <= 16
&& (isMonochrome || chromaBitDepth == lumaBitDepth)
&& mode == HeifColorConversionMode.Coefficients;
/// <summary>
/// Converts supported high-bit-depth HEVC planes using pinned libheif arithmetic and nearest chroma sampling.
/// </summary>
/// <typeparam name="TPixel">The destination pixel type.</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="buffer">The reconstructed component-plane buffer.</param>
/// <param name="image">The destination image frame.</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="sourceY">The vertical luma-sample offset of the output window.</param>
public static void Convert<TPixel, TBuffer>(
Configuration configuration,
TBuffer buffer,
ImageFrame<TPixel> image,
in HeifColorConversionParameters parameters,
int sourceX,
int sourceY)
where TPixel : unmanaged, IPixel<TPixel>
where TBuffer : struct, IHeifPlanarSampleBuffer<ushort>
{
ConversionParameters conversionParameters = new(in parameters, buffer.LumaBitDepth);
// Three planar rows and one packed Rgba64 row share a single image-lifetime allocation. The latter occupies
// four UInt16 values per pixel, so the complete scratch requirement is seven samples per output pixel.
using IMemoryOwner<ushort> rowOwner = configuration.MemoryAllocator.Allocate<ushort>(image.Width * 7);
Span<ushort> storage = rowOwner.GetSpan();
Span<ushort> red = storage[..image.Width];
Span<ushort> green = storage.Slice(image.Width, image.Width);
Span<ushort> blue = storage.Slice(image.Width * 2, image.Width);
Span<Rgba64> packed = MemoryMarshal.Cast<ushort, Rgba64>(storage[(image.Width * 3)..]);
for (int y = 0; y < image.Height; y++)
{
int lumaY = sourceY + y;
ReadOnlySpan<ushort> luma = buffer.GetLumaRowSpan(lumaY).Slice(sourceX, image.Width);
if (buffer.IsMonochrome)
{
// Pinned libheif copies the reconstructed luma code value directly to RGB for monochrome images.
// Scaling to the 16-bit pixel domain happens after that copy, without limited-range expansion.
ConvertRow<LibheifMonochromeOperator>(
luma,
luma,
luma,
red,
green,
blue,
0,
in conversionParameters);
}
else
{
int subsamplingX = buffer.ChromaSubsamplingX;
int chromaY = lumaY >> buffer.ChromaSubsamplingY;
ReadOnlySpan<ushort> chromaBlue = buffer.GetChromaBlueRowSpan(chromaY).Slice(sourceX >> subsamplingX);
ReadOnlySpan<ushort> chromaRed = buffer.GetChromaRedRowSpan(chromaY).Slice(sourceX >> subsamplingX);
// libheif's selected direct conversion addresses the native chroma sample at x >> subsamplingX.
// The HEIF crop boundary already keeps sourceX aligned to complete chroma samples.
ConvertRow<LibheifCoefficientOperator>(
luma,
chromaBlue,
chromaRed,
red,
green,
blue,
subsamplingX,
in conversionParameters);
}
HeifSampleConversion.PackRgba64(red, green, blue, packed);
Span<TPixel> destination = image.PixelBuffer.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.FromRgba64(configuration, packed, destination);
}
}
}

125
src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.LibheifCoefficientOperator.cs

@ -1,125 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Components;
/// <content>
/// Provides the coefficient conversion executed by pinned libheif 1.23.1. Each SIMD lane carries one output pixel.
/// Range expansion and matrix arithmetic remain in single precision, RGB is rounded and clipped at the coded
/// precision, and the final integer shift reproduces libheif's separate high-bit-depth-to-eight-bit operation.
/// </content>
internal static partial class HeifYuvToRgb8Converter
{
/// <summary>
/// Implements pinned-libheif coefficient conversion for scalar and SIMD lanes.
/// </summary>
private readonly struct LibheifCoefficientOperator : IHeifYuvToRgb8Operator
{
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Convert(
Vector512<int> y,
Vector512<int> cb,
Vector512<int> cr,
in ConversionParameters parameters,
out Vector512<int> r,
out Vector512<int> g,
out Vector512<int> b)
{
LibheifVector512Parameters values = parameters.LibheifSixteenLane;
Vector512<float> luma = (Vector512.ConvertToSingle(y) - values.LumaOffset) * values.LumaScale;
Vector512<float> blueDifference = (Vector512.ConvertToSingle(cb) - values.ChromaMidpoint) * values.ChromaScale;
Vector512<float> redDifference = (Vector512.ConvertToSingle(cr) - values.ChromaMidpoint) * values.ChromaScale;
Vector512<float> half = Vector512.Create(0.5F);
// Sixteen independent samples use the same float32 ordering as the narrower paths. The closed operator
// keeps this compatibility arithmetic outside the row dispatch while allowing an exact scalar fallback.
Vector512<int> red = Vector512.ConvertToInt32(Vector512.Truncate(luma + (values.RedCr * redDifference) + half));
Vector512<int> green = Vector512.ConvertToInt32(Vector512.Truncate(luma + (values.GreenCb * blueDifference) + (values.GreenCr * redDifference) + half));
Vector512<int> blue = Vector512.ConvertToInt32(Vector512.Truncate(luma + (values.BlueCb * blueDifference) + half));
r = Vector512.Clamp(red, default, values.Maximum) >> values.OutputShift;
g = Vector512.Clamp(green, default, values.Maximum) >> values.OutputShift;
b = Vector512.Clamp(blue, default, values.Maximum) >> values.OutputShift;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Convert(
Vector256<int> y,
Vector256<int> cb,
Vector256<int> cr,
in ConversionParameters parameters,
out Vector256<int> r,
out Vector256<int> g,
out Vector256<int> b)
{
LibheifVector256Parameters values = parameters.LibheifEightLane;
Vector256<float> luma = (Vector256.ConvertToSingle(y) - values.LumaOffset) * values.LumaScale;
Vector256<float> blueDifference = (Vector256.ConvertToSingle(cb) - values.ChromaMidpoint) * values.ChromaScale;
Vector256<float> redDifference = (Vector256.ConvertToSingle(cr) - values.ChromaMidpoint) * values.ChromaScale;
Vector256<float> half = Vector256.Create(0.5F);
// Eight YUV tuples remain planar across the YMM arithmetic. The expression association matches the
// pinned scalar source, including the two successive green additions before truncation.
Vector256<int> red = Vector256.ConvertToInt32(Vector256.Truncate(luma + (values.RedCr * redDifference) + half));
Vector256<int> green = Vector256.ConvertToInt32(Vector256.Truncate(luma + (values.GreenCb * blueDifference) + (values.GreenCr * redDifference) + half));
Vector256<int> blue = Vector256.ConvertToInt32(Vector256.Truncate(luma + (values.BlueCb * blueDifference) + half));
r = Vector256.Clamp(red, default, values.Maximum) >> values.OutputShift;
g = Vector256.Clamp(green, default, values.Maximum) >> values.OutputShift;
b = Vector256.Clamp(blue, default, values.Maximum) >> values.OutputShift;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Convert(
Vector128<int> y,
Vector128<int> cb,
Vector128<int> cr,
in ConversionParameters parameters,
out Vector128<int> r,
out Vector128<int> g,
out Vector128<int> b)
{
LibheifVector128Parameters values = parameters.LibheifFourLane;
Vector128<float> luma = (Vector128.ConvertToSingle(y) - values.LumaOffset) * values.LumaScale;
Vector128<float> blueDifference = (Vector128.ConvertToSingle(cb) - values.ChromaMidpoint) * values.ChromaScale;
Vector128<float> redDifference = (Vector128.ConvertToSingle(cr) - values.ChromaMidpoint) * values.ChromaScale;
Vector128<float> half = Vector128.Create(0.5F);
// Truncate after the explicit half-unit bias to mirror C++ float-to-int conversion. Clipping in integer
// lanes then preserves the source-precision boundary before the common eight-bit reduction shift.
Vector128<int> red = Vector128.ConvertToInt32(Vector128.Truncate(luma + (values.RedCr * redDifference) + half));
Vector128<int> green = Vector128.ConvertToInt32(Vector128.Truncate(luma + (values.GreenCb * blueDifference) + (values.GreenCr * redDifference) + half));
Vector128<int> blue = Vector128.ConvertToInt32(Vector128.Truncate(luma + (values.BlueCb * blueDifference) + half));
r = Vector128.Clamp(red, default, values.Maximum) >> values.OutputShift;
g = Vector128.Clamp(green, default, values.Maximum) >> values.OutputShift;
b = Vector128.Clamp(blue, default, values.Maximum) >> values.OutputShift;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Convert(ushort y, ushort cb, ushort cr, in ConversionParameters parameters, out byte r, out byte g, out byte b)
{
LibheifParameters values = parameters.LibheifScalar;
float luma = (y - values.LumaOffset) * values.LumaScale;
float blueDifference = (cb - values.ChromaMidpoint) * values.ChromaScale;
float redDifference = (cr - values.ChromaMidpoint) * values.ChromaScale;
// libheif's clip_f_u16 adds one half, truncates toward zero, and then clips. RGB is rounded before
// the high-bit-depth plane is reduced, so moving the shift into the floating-point scale changes bytes.
int red = (int)(luma + (values.RedCr * redDifference) + 0.5F);
int green = (int)(luma + (values.GreenCb * blueDifference) + (values.GreenCr * redDifference) + 0.5F);
int blue = (int)(luma + (values.BlueCb * blueDifference) + 0.5F);
r = (byte)(Numerics.Clamp(red, 0, values.Maximum) >> values.OutputShift);
g = (byte)(Numerics.Clamp(green, 0, values.Maximum) >> values.OutputShift);
b = (byte)(Numerics.Clamp(blue, 0, values.Maximum) >> values.OutputShift);
}
}
}

82
src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.LibheifMonochromeOperator.cs

@ -1,82 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Components;
/// <content>
/// Provides pinned-libheif monochrome presentation. The luma code value is reduced directly to eight bits and copied
/// to all RGB components; signaled luma-range expansion is intentionally absent because libheif's direct monochrome
/// operation does not apply it.
/// </content>
internal static partial class HeifYuvToRgb8Converter
{
/// <summary>
/// Implements pinned-libheif monochrome conversion for scalar and SIMD lanes.
/// </summary>
private readonly struct LibheifMonochromeOperator : IHeifYuvToRgb8Operator
{
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Convert(
Vector512<int> y,
Vector512<int> cb,
Vector512<int> cr,
in ConversionParameters parameters,
out Vector512<int> r,
out Vector512<int> g,
out Vector512<int> b)
{
Vector512<int> value = y >> parameters.LibheifSixteenLane.OutputShift;
r = value;
g = value;
b = value;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Convert(
Vector256<int> y,
Vector256<int> cb,
Vector256<int> cr,
in ConversionParameters parameters,
out Vector256<int> r,
out Vector256<int> g,
out Vector256<int> b)
{
Vector256<int> value = y >> parameters.LibheifEightLane.OutputShift;
r = value;
g = value;
b = value;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Convert(
Vector128<int> y,
Vector128<int> cb,
Vector128<int> cr,
in ConversionParameters parameters,
out Vector128<int> r,
out Vector128<int> g,
out Vector128<int> b)
{
Vector128<int> value = y >> parameters.LibheifFourLane.OutputShift;
r = value;
g = value;
b = value;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Convert(ushort y, ushort cb, ushort cr, in ConversionParameters parameters, out byte r, out byte g, out byte b)
{
byte value = (byte)(y >> parameters.LibheifScalar.OutputShift);
r = value;
g = value;
b = value;
}
}
}

281
src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.Parameters.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
namespace SixLabors.ImageSharp.Formats.Heif.Components;
@ -36,44 +35,17 @@ internal static partial class HeifYuvToRgb8Converter
/// </summary>
public readonly Vector512Parameters FixedPointSixteenLane;
/// <summary>
/// The scalar pinned-libheif conversion parameters.
/// </summary>
public readonly LibheifParameters LibheifScalar;
/// <summary>
/// The four-lane pinned-libheif conversion parameters.
/// </summary>
public readonly LibheifVector128Parameters LibheifFourLane;
/// <summary>
/// The eight-lane pinned-libheif conversion parameters.
/// </summary>
public readonly LibheifVector256Parameters LibheifEightLane;
/// <summary>
/// The sixteen-lane pinned-libheif conversion parameters.
/// </summary>
public readonly LibheifVector512Parameters LibheifSixteenLane;
/// <summary>
/// Initializes a new instance of the <see cref="ConversionParameters"/> struct.
/// </summary>
/// <param name="parameters">The shared floating-point conversion parameters.</param>
/// <param name="bitDepth">The common source component precision.</param>
public ConversionParameters(in HeifColorConversionParameters parameters, int bitDepth)
public ConversionParameters(in HeifColorConversionParameters parameters)
{
FixedPointParameters scalar = new(in parameters);
this.FixedPointScalar = scalar;
this.FixedPointFourLane = new(in scalar);
this.FixedPointEightLane = new(in scalar);
this.FixedPointSixteenLane = new(in scalar);
LibheifParameters libheif = new(in parameters, bitDepth);
this.LibheifScalar = libheif;
this.LibheifFourLane = new(in libheif);
this.LibheifEightLane = new(in libheif);
this.LibheifSixteenLane = new(in libheif);
}
}
@ -286,255 +258,4 @@ internal static partial class HeifYuvToRgb8Converter
/// </summary>
public Vector512<int> BlueCb { get; }
}
/// <summary>
/// Stores the scalar coefficients and range values used by pinned libheif 1.23.1.
/// </summary>
private readonly struct LibheifParameters
{
/// <summary>
/// Initializes a new instance of the <see cref="LibheifParameters"/> struct.
/// </summary>
/// <param name="parameters">The resolved H.273 matrix and range values.</param>
/// <param name="bitDepth">The common source component precision.</param>
public LibheifParameters(in HeifColorConversionParameters parameters, int bitDepth)
{
this.LumaOffset = parameters.IsFullRange ? 0F : parameters.LumaBias;
this.LumaScale = parameters.IsFullRange ? 1F : 1.1689F;
this.ChromaMidpoint = parameters.ChromaBias;
this.ChromaScale = parameters.IsFullRange ? 1F : 1.1429F;
if (parameters.MatrixCoefficients == CicpMatrixCoefficients.Unspecified)
{
// libheif falls back to these literal Rec.601 coefficients when no matrix is signaled.
this.RedCr = 1.402F;
this.GreenCb = -0.344136F;
this.GreenCr = -0.714136F;
this.BlueCb = 1.772F;
}
else
{
float kr = parameters.Kr;
float kb = parameters.Kb;
this.RedCr = 2F * (-kr + 1F);
this.GreenCb = 2F * kb * (-kb + 1F) / (kb + kr - 1F);
this.GreenCr = 2F * kr * (-kr + 1F) / (kb + kr - 1F);
this.BlueCb = 2F * (-kb + 1F);
}
this.Maximum = (1 << bitDepth) - 1;
this.OutputShift = bitDepth - 8;
}
/// <summary>
/// Gets the luma code-value offset removed before limited-range expansion.
/// </summary>
public float LumaOffset { get; }
/// <summary>
/// Gets the luma range-expansion factor.
/// </summary>
public float LumaScale { get; }
/// <summary>
/// Gets the neutral chroma code value.
/// </summary>
public float ChromaMidpoint { get; }
/// <summary>
/// Gets the chroma range-expansion factor.
/// </summary>
public float ChromaScale { get; }
/// <summary>
/// Gets the red contribution from Cr.
/// </summary>
public float RedCr { get; }
/// <summary>
/// Gets the green contribution from Cb.
/// </summary>
public float GreenCb { get; }
/// <summary>
/// Gets the green contribution from Cr.
/// </summary>
public float GreenCr { get; }
/// <summary>
/// Gets the blue contribution from Cb.
/// </summary>
public float BlueCb { get; }
/// <summary>
/// Gets the largest RGB code value at the source precision.
/// </summary>
public int Maximum { get; }
/// <summary>
/// Gets the right shift reducing source-precision RGB to eight bits.
/// </summary>
public int OutputShift { get; }
}
/// <summary>
/// Broadcasts pinned-libheif coefficients for four-lane conversion.
/// </summary>
private readonly struct LibheifVector128Parameters
{
/// <summary>
/// Initializes a new instance of the <see cref="LibheifVector128Parameters"/> struct.
/// </summary>
/// <param name="parameters">The scalar pinned-libheif coefficients.</param>
public LibheifVector128Parameters(in LibheifParameters parameters)
{
this.LumaOffset = Vector128.Create(parameters.LumaOffset);
this.LumaScale = Vector128.Create(parameters.LumaScale);
this.ChromaMidpoint = Vector128.Create(parameters.ChromaMidpoint);
this.ChromaScale = Vector128.Create(parameters.ChromaScale);
this.RedCr = Vector128.Create(parameters.RedCr);
this.GreenCb = Vector128.Create(parameters.GreenCb);
this.GreenCr = Vector128.Create(parameters.GreenCr);
this.BlueCb = Vector128.Create(parameters.BlueCb);
this.Maximum = Vector128.Create(parameters.Maximum);
this.OutputShift = parameters.OutputShift;
}
/// <summary>Gets the luma offset lanes.</summary>
public Vector128<float> LumaOffset { get; }
/// <summary>Gets the luma scale lanes.</summary>
public Vector128<float> LumaScale { get; }
/// <summary>Gets the chroma-midpoint lanes.</summary>
public Vector128<float> ChromaMidpoint { get; }
/// <summary>Gets the chroma-scale lanes.</summary>
public Vector128<float> ChromaScale { get; }
/// <summary>Gets the red Cr coefficient lanes.</summary>
public Vector128<float> RedCr { get; }
/// <summary>Gets the green Cb coefficient lanes.</summary>
public Vector128<float> GreenCb { get; }
/// <summary>Gets the green Cr coefficient lanes.</summary>
public Vector128<float> GreenCr { get; }
/// <summary>Gets the blue Cb coefficient lanes.</summary>
public Vector128<float> BlueCb { get; }
/// <summary>Gets the maximum RGB code-value lanes.</summary>
public Vector128<int> Maximum { get; }
/// <summary>Gets the output reduction shift.</summary>
public int OutputShift { get; }
}
/// <summary>
/// Broadcasts pinned-libheif coefficients for eight-lane conversion.
/// </summary>
private readonly struct LibheifVector256Parameters
{
/// <summary>
/// Initializes a new instance of the <see cref="LibheifVector256Parameters"/> struct.
/// </summary>
/// <param name="parameters">The scalar pinned-libheif coefficients.</param>
public LibheifVector256Parameters(in LibheifParameters parameters)
{
this.LumaOffset = Vector256.Create(parameters.LumaOffset);
this.LumaScale = Vector256.Create(parameters.LumaScale);
this.ChromaMidpoint = Vector256.Create(parameters.ChromaMidpoint);
this.ChromaScale = Vector256.Create(parameters.ChromaScale);
this.RedCr = Vector256.Create(parameters.RedCr);
this.GreenCb = Vector256.Create(parameters.GreenCb);
this.GreenCr = Vector256.Create(parameters.GreenCr);
this.BlueCb = Vector256.Create(parameters.BlueCb);
this.Maximum = Vector256.Create(parameters.Maximum);
this.OutputShift = parameters.OutputShift;
}
/// <summary>Gets the luma offset lanes.</summary>
public Vector256<float> LumaOffset { get; }
/// <summary>Gets the luma scale lanes.</summary>
public Vector256<float> LumaScale { get; }
/// <summary>Gets the chroma-midpoint lanes.</summary>
public Vector256<float> ChromaMidpoint { get; }
/// <summary>Gets the chroma-scale lanes.</summary>
public Vector256<float> ChromaScale { get; }
/// <summary>Gets the red Cr coefficient lanes.</summary>
public Vector256<float> RedCr { get; }
/// <summary>Gets the green Cb coefficient lanes.</summary>
public Vector256<float> GreenCb { get; }
/// <summary>Gets the green Cr coefficient lanes.</summary>
public Vector256<float> GreenCr { get; }
/// <summary>Gets the blue Cb coefficient lanes.</summary>
public Vector256<float> BlueCb { get; }
/// <summary>Gets the maximum RGB code-value lanes.</summary>
public Vector256<int> Maximum { get; }
/// <summary>Gets the output reduction shift.</summary>
public int OutputShift { get; }
}
/// <summary>
/// Broadcasts pinned-libheif coefficients for sixteen-lane conversion.
/// </summary>
private readonly struct LibheifVector512Parameters
{
/// <summary>
/// Initializes a new instance of the <see cref="LibheifVector512Parameters"/> struct.
/// </summary>
/// <param name="parameters">The scalar pinned-libheif coefficients.</param>
public LibheifVector512Parameters(in LibheifParameters parameters)
{
this.LumaOffset = Vector512.Create(parameters.LumaOffset);
this.LumaScale = Vector512.Create(parameters.LumaScale);
this.ChromaMidpoint = Vector512.Create(parameters.ChromaMidpoint);
this.ChromaScale = Vector512.Create(parameters.ChromaScale);
this.RedCr = Vector512.Create(parameters.RedCr);
this.GreenCb = Vector512.Create(parameters.GreenCb);
this.GreenCr = Vector512.Create(parameters.GreenCr);
this.BlueCb = Vector512.Create(parameters.BlueCb);
this.Maximum = Vector512.Create(parameters.Maximum);
this.OutputShift = parameters.OutputShift;
}
/// <summary>Gets the luma offset lanes.</summary>
public Vector512<float> LumaOffset { get; }
/// <summary>Gets the luma scale lanes.</summary>
public Vector512<float> LumaScale { get; }
/// <summary>Gets the chroma-midpoint lanes.</summary>
public Vector512<float> ChromaMidpoint { get; }
/// <summary>Gets the chroma-scale lanes.</summary>
public Vector512<float> ChromaScale { get; }
/// <summary>Gets the red Cr coefficient lanes.</summary>
public Vector512<float> RedCr { get; }
/// <summary>Gets the green Cb coefficient lanes.</summary>
public Vector512<float> GreenCb { get; }
/// <summary>Gets the green Cr coefficient lanes.</summary>
public Vector512<float> GreenCr { get; }
/// <summary>Gets the blue Cb coefficient lanes.</summary>
public Vector512<float> BlueCb { get; }
/// <summary>Gets the maximum RGB code-value lanes.</summary>
public Vector512<int> Maximum { get; }
/// <summary>Gets the output reduction shift.</summary>
public int OutputShift { get; }
}
}

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

@ -56,28 +56,6 @@ internal static partial class HeifYuvToRgb8Converter
&& matrixCoefficients == CicpMatrixCoefficients.Unspecified
&& mode == HeifColorConversionMode.Coefficients;
/// <summary>
/// Determines whether the pinned libheif-compatible conversion supports the supplied plane and color description.
/// </summary>
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param>
/// <param name="subsamplingY">The vertical chroma subsampling shift.</param>
/// <param name="lumaBitDepth">The luma sample precision in bits.</param>
/// <param name="chromaBitDepth">The chroma sample precision in bits.</param>
/// <param name="isMonochrome">Whether the image contains only luma samples.</param>
/// <param name="mode">The resolved H.273 conversion operation.</param>
/// <returns><see langword="true"/> when the planes can use this converter; otherwise, <see langword="false"/>.</returns>
public static bool SupportsLibheifConversion(
int subsamplingX,
int subsamplingY,
int lumaBitDepth,
int chromaBitDepth,
bool isMonochrome,
HeifColorConversionMode mode)
=> (isMonochrome || (subsamplingX is 0 or 1 && subsamplingY is 0 or 1))
&& lumaBitDepth == 8
&& (isMonochrome || chromaBitDepth == 8)
&& mode == HeifColorConversionMode.Coefficients;
/// <summary>
/// Converts supported HEIF component planes to packed pixels using integer SIMD with a scalar tail.
/// </summary>
@ -99,7 +77,7 @@ internal static partial class HeifYuvToRgb8Converter
where TPixel : unmanaged, IPixel<TPixel>
where TBuffer : struct, IHeifPlanarSampleBuffer<ushort>
{
ConversionParameters conversionParameters = new(in parameters, 8);
ConversionParameters conversionParameters = new(in parameters);
using IMemoryOwner<byte> componentOwner = configuration.MemoryAllocator.Allocate<byte>(image.Width * 3);
Span<byte> components = componentOwner.GetSpan();
Span<byte> red = components[..image.Width];
@ -132,76 +110,4 @@ internal static partial class HeifYuvToRgb8Converter
PixelOperations<TPixel>.Instance.PackFromRgbPlanes(red, green, blue, destination);
}
}
/// <summary>
/// Converts supported HEVC planes with the arithmetic and nearest-sample traversal used by pinned libheif.
/// </summary>
/// <typeparam name="TPixel">The destination pixel type.</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="buffer">The reconstructed component-plane buffer.</param>
/// <param name="image">The destination image frame.</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="sourceY">The vertical luma-sample offset of the output window.</param>
public static void ConvertLibheif<TPixel, TBuffer>(
Configuration configuration,
TBuffer buffer,
ImageFrame<TPixel> image,
in HeifColorConversionParameters parameters,
int sourceX,
int sourceY)
where TPixel : unmanaged, IPixel<TPixel>
where TBuffer : struct, IHeifPlanarSampleBuffer<ushort>
{
ConversionParameters conversionParameters = new(in parameters, buffer.LumaBitDepth);
using IMemoryOwner<byte> componentOwner = configuration.MemoryAllocator.Allocate<byte>(image.Width * 3);
Span<byte> components = componentOwner.GetSpan();
Span<byte> red = components[..image.Width];
Span<byte> green = components.Slice(image.Width, image.Width);
Span<byte> blue = components.Slice(image.Width * 2, image.Width);
for (int y = 0; y < image.Height; y++)
{
int lumaY = sourceY + y;
ReadOnlySpan<ushort> luma = buffer.GetLumaRowSpan(lumaY).Slice(sourceX, image.Width);
if (buffer.IsMonochrome)
{
// Pinned libheif reduces monochrome precision first and copies that code value to RGB. It does not
// apply the signaled limited-range expansion used by its three-component conversion operation.
ConvertRow<LibheifMonochromeOperator>(
luma,
luma,
luma,
red,
green,
blue,
0,
in conversionParameters);
}
else
{
int subsamplingX = buffer.ChromaSubsamplingX;
int chromaY = lumaY >> buffer.ChromaSubsamplingY;
// The HEIF crop boundary validates horizontal offsets in complete chroma-sample units. Slicing once
// therefore preserves libheif's x >> subsampling mapping without a phase branch in the SIMD loop.
ReadOnlySpan<ushort> chromaBlue = buffer.GetChromaBlueRowSpan(chromaY).Slice(sourceX >> subsamplingX);
ReadOnlySpan<ushort> chromaRed = buffer.GetChromaRedRowSpan(chromaY).Slice(sourceX >> subsamplingX);
ConvertRow<LibheifCoefficientOperator>(
luma,
chromaBlue,
chromaRed,
red,
green,
blue,
subsamplingX,
in conversionParameters);
}
Span<TPixel> destination = image.PixelBuffer.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.PackFromRgbPlanes(red, green, blue, destination);
}
}
}

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

@ -238,11 +238,6 @@ public enum Heif4CharCode : uint
/// </summary>
Ndwt = 0x6E647774U,
/// <summary>
/// HEVC codec-configuration item property.
/// </summary>
HvcC = 0x68766343U,
/// <summary>
/// AV1 configuration.
/// </summary>
@ -323,36 +318,6 @@ public enum Heif4CharCode : uint
/// </summary>
Ipma = 0x69706D61U,
/// <summary>
/// High Efficient Image Coding brand.
/// </summary>
Heic = 0x68656963U,
/// <summary>
/// High Efficient Image Coding brand (legacy name).
/// </summary>
Heix = 0x68656978U,
/// <summary>
/// HEVC image sequence brand.
/// </summary>
Hevc = 0x68657663U,
/// <summary>
/// HEVC Main 10 image sequence brand.
/// </summary>
Hevx = 0x68657678U,
/// <summary>
/// Layered HEVC image sequence brand.
/// </summary>
Hevm = 0x6865766DU,
/// <summary>
/// Layered HEVC image sequence brand.
/// </summary>
Hevs = 0x68657673U,
/// <summary>
/// High Efficient File brand.
/// </summary>
@ -383,21 +348,6 @@ public enum Heif4CharCode : uint
/// </summary>
Iso8 = 0x69736F38U,
/// <summary>
/// HEVC-coded image item.
/// </summary>
Hvc1 = 0x68766331U,
/// <summary>
/// Layered High Efficiency Coding sample.
/// </summary>
Hvc2 = 0x68766332U,
/// <summary>
/// Layered High Efficiency Coding sample.
/// </summary>
Lhv1 = 0x6C687631U,
/// <summary>
/// Legacy JPEG coded tile.
/// </summary>

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

@ -52,7 +52,6 @@
"amve", "Ambient viewing environment",
"reve", "Reference viewing environment",
"ndwt", "Nominal diffuse white",
"hvcC", "HVC configuration",
"av1C", "AV1 configuration",
"a1op", "AV1 operating-point selector",
"lsel", "AV1 layer selector",
@ -69,21 +68,12 @@
"ipmc", "IPMP Control Box",
"ipco", "Item Property Container",
"ipma", "Item Property Association",
"heic", "High Efficient Image Coding brand",
"heix", "High Efficient Image Coding brand (legacy name)",
"hevc", "HEVC image sequence brand",
"hevx", "HEVC Main 10 image sequence brand",
"hevm", "Layered HEVC image sequence brand",
"hevs", "Layered HEVC image sequence brand",
"mif1", "High Efficient File brand",
"avif", "AVIF brand",
"avis", "AVIF image sequence brand",
"avio", "AVIF intra-only image sequence brand",
"msf1", "HEIF image sequence structural brand",
"iso8", "ISO base media version 8 structural brand",
"hvc1", "High Efficiency Coding tile",
"hvc2", "Layered High Efficiency Coding sample",
"lhv1", "Layered High Efficiency Coding sample",
"jpeg", "Legacy JPEG coded tile",
"jpgs", "JPEG image sequence brand",
"av01", "AOMedia Video Coding tile",

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

@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Hevc;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Heif;
@ -22,7 +21,6 @@ internal static class HeifCompressionFactory
{
Heif4CharCode.Jpeg => new JpegHeifItemDecoder<TPixel>(),
Heif4CharCode.Av01 => new Av1HeifItemDecoder<TPixel>(),
Heif4CharCode.Hvc1 => new HevcHeifItemDecoder<TPixel>(),
_ => null
};
}

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

@ -8,11 +8,6 @@ namespace SixLabors.ImageSharp.Formats.Heif;
/// </summary>
public enum HeifCompressionMethod
{
/// <summary>
/// High Efficiency Video Coding (HEVC).
/// </summary>
Hevc,
/// <summary>
/// Legacy JPEG coding.
/// </summary>

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

@ -10,30 +10,20 @@ namespace SixLabors.ImageSharp.Formats.Heif;
/// </summary>
internal static class HeifConstants
{
/// <summary>
/// The HEIC still-image brand written by the encoder.
/// </summary>
public const Heif4CharCode HeicBrand = Heif4CharCode.Heic;
/// <summary>
/// The auxiliary-type URN used by current HEIF alpha image items.
/// </summary>
public const string AlphaAuxiliaryType = "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha";
/// <summary>
/// The auxiliary-type URN used by legacy HEVC alpha image items.
/// </summary>
public const string LegacyAlphaAuxiliaryType = "urn:mpeg:hevc:2015:auxid:1";
/// <summary>
/// The list of mimetypes that equate to a HEIC.
/// The MIME types recognized by this HEIF implementation.
/// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/heif", "image/heic", "image/avif" };
public static readonly IEnumerable<string> MimeTypes = new[] { "image/heif", "image/avif" };
/// <summary>
/// The list of file extensions that equate to a HEIC.
/// The file extensions recognized by this HEIF implementation.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "heic", "heif", "hif", "avif" };
public static readonly IEnumerable<string> FileExtensions = new[] { "heif", "hif", "avif" };
/// <summary>
/// Determines the supported image presentation declared by a file-type box.
@ -98,9 +88,9 @@ internal static class HeifConstants
/// Determines whether an auxiliary-type property identifies an alpha image plane.
/// </summary>
/// <param name="auxiliaryType">The null-terminated auxiliary type decoded from an <c>auxC</c> property.</param>
/// <returns><see langword="true"/> when the type is either registered HEIF alpha URN.</returns>
/// <returns><see langword="true"/> when the type is the registered HEIF alpha URN.</returns>
public static bool IsAlphaAuxiliaryType(string? auxiliaryType)
=> auxiliaryType is AlphaAuxiliaryType or LegacyAlphaAuxiliaryType;
=> auxiliaryType == AlphaAuxiliaryType;
/// <summary>
/// Determines whether <paramref name="brand"/> identifies a still-image container supported by this codec.
@ -108,9 +98,7 @@ internal static class HeifConstants
/// <param name="brand">The registered file-type brand.</param>
/// <returns><see langword="true"/> when the brand identifies a supported still-image container.</returns>
private static bool IsSupportedStillImageBrand(Heif4CharCode brand)
=> brand is Heif4CharCode.Heic
or Heif4CharCode.Heix
or Heif4CharCode.Mif1
=> brand is Heif4CharCode.Mif1
or Heif4CharCode.Avif
or Heif4CharCode.Jpeg;
@ -120,17 +108,13 @@ internal static class HeifConstants
/// <param name="brand">The registered file-type brand.</param>
/// <returns><see langword="true"/> when the brand identifies a supported timed image sequence.</returns>
private static bool IsSupportedSequenceBrand(Heif4CharCode brand)
=> brand is Heif4CharCode.Hevc
or Heif4CharCode.Hevx
or 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 layered HEVC or JPEG sequence support.</returns>
/// <returns><see langword="true"/> when the major brand requires unsupported JPEG sequence support.</returns>
private static bool IsUnsupportedSequenceBrand(Heif4CharCode brand)
=> brand is Heif4CharCode.Hevm
or Heif4CharCode.Hevs
or Heif4CharCode.Jpgs;
=> brand is Heif4CharCode.Jpgs;
}

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

@ -7,7 +7,6 @@ using System.Text;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Components.Alpha;
using SixLabors.ImageSharp.Formats.Heif.Hevc;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
@ -254,7 +253,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
}
/// <summary>
/// Locates and parses the single movie box of a supported HEIC or AVIF image sequence.
/// Locates and parses the single movie box of a supported AVIF image sequence.
/// </summary>
/// <param name="stream">The complete container stream positioned after its file-type box.</param>
/// <returns>The bounded selected image-sequence model.</returns>
@ -284,7 +283,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
}
/// <summary>
/// Creates image and frame metadata from a parsed HEIC or AVIF image sequence without decoding its samples.
/// Creates image and frame metadata from a parsed AVIF image sequence without decoding its samples.
/// </summary>
/// <param name="sequence">The parsed selected image sequence.</param>
/// <returns>The identified dimensions and bounded visible-frame metadata.</returns>
@ -298,7 +297,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
}
/// <summary>
/// Decodes the retained visible samples of a HEIC or AVIF image sequence into one multi-frame image.
/// Decodes the retained visible samples of an AVIF image sequence into one multi-frame image.
/// </summary>
/// <typeparam name="TPixel">The destination pixel format.</typeparam>
/// <param name="stream">The complete seekable HEIF stream.</param>
@ -653,14 +652,6 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
heifMetadata.BitDepth = av1Configuration.BitDepth;
heifMetadata.IsMonochrome = av1Configuration.IsMonochrome;
break;
case Heif4CharCode.Hvc1:
HevcCodecConfiguration hevcConfiguration = colorTrack.HevcCodecConfiguration
?? throw new InvalidImageContentException("The HEVC image-sequence track has no codec configuration.");
heifMetadata.CompressionMethod = HeifCompressionMethod.Hevc;
heifMetadata.BitDepth = hevcConfiguration.BitDepth;
heifMetadata.IsMonochrome = hevcConfiguration.IsMonochrome;
break;
default:
throw new InvalidImageContentException($"The image-sequence sample entry '{colorTrack.CodecType}' is not supported.");
}
@ -785,7 +776,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
}
HeifMetadata meta = metadata.GetHeifMetadata();
HeifCompressionMethod compressionMethod = HeifCompressionMethod.Hevc;
HeifCompressionMethod compressionMethod;
if (metadataItem.Type == Heif4CharCode.Av01)
{
Av1CodecConfiguration codecConfiguration = metadataItem.Av1CodecConfiguration
@ -795,23 +786,14 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
meta.BitDepth = codecConfiguration.BitDepth;
meta.IsMonochrome = codecConfiguration.IsMonochrome;
}
else if (metadataItem.Type == Heif4CharCode.Hvc1)
{
HevcCodecConfiguration codecConfiguration = metadataItem.HevcCodecConfiguration
?? throw new InvalidImageContentException($"HEVC image item {metadataItem.Id} has no codec configuration property.");
if (metadataItem.ChannelBitDepths is not null)
{
codecConfiguration.ValidateChannelBitDepths(metadataItem.ChannelBitDepths);
}
meta.BitDepth = codecConfiguration.BitDepth;
meta.IsMonochrome = codecConfiguration.IsMonochrome;
}
else if (metadataItem.Type == Heif4CharCode.Jpeg)
{
compressionMethod = HeifCompressionMethod.LegacyJpeg;
}
else
{
throw new InvalidImageContentException($"Image item {metadataItem.Id} uses unsupported item type '{metadataItem.Type}'.");
}
meta.CompressionMethod = compressionMethod;
meta.HasAlpha = this.FindAlphaItem(presentationItem) is not null
@ -1511,13 +1493,6 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
Heif4CharCode.A1lx,
HeifPropertyParser.ParseAv1LayeredImageIndex(boxBuffer)));
break;
case Heif4CharCode.HvcC:
properties.Add(
new KeyValuePair<Heif4CharCode, object>(
Heif4CharCode.HvcC,
new HevcCodecConfiguration(boxBuffer)));
break;
case Heif4CharCode.Clap:
properties.Add(
@ -1799,29 +1774,6 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
item.Av1LayeredImageIndex = layeredImageIndex;
}
break;
case Heif4CharCode.HvcC:
if (prop.Value is HevcCodecConfiguration hevcCodecConfiguration)
{
if (item.Type != Heif4CharCode.Hvc1)
{
this.ThrowOrIgnoreImageDataSegmentError(
$"Item {itemId} associates an HEVC codec configuration with non-HEVC item type '{item.Type}'.");
break;
}
if (item.HevcCodecConfiguration is not null)
{
this.ThrowOrIgnoreImageDataSegmentError(
$"Item {itemId} associates more than one HEVC codec configuration property.");
break;
}
item.HevcCodecConfiguration = hevcCodecConfiguration;
}
break;
case Heif4CharCode.AuxC:
if (prop.Value is string auxiliaryType)
@ -2352,15 +2304,8 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
this.ApplyAssociatedMetadata(image.Metadata, rootItem, buffers);
}
if (itemDecoder is HevcHeifItemDecoder<TPixel> hevcItemDecoder)
{
// The codec orientation describes the complete cropped picture. Item scaling and alpha composition
// must finish first so rotation neither resizes back to ispe nor leaves the auxiliary plane unrotated.
hevcItemDecoder.ApplySupplementalPresentation(image);
}
// MIAF defines crop, rotation, and mirror as presentation operations in that order. Applying the
// container transforms after codec presentation keeps every composed plane in the same coordinate space.
// container transforms after item composition keeps every composed plane in the same coordinate space.
ApplyPresentationTransforms(image, itemToDecode);
if (!this.Options.SkipMetadata)

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

@ -52,7 +52,6 @@ internal sealed class HeifEncoderCore
{
HeifCompressionMethod.LegacyJpeg => this.CompressPixels(image, cancellationToken),
HeifCompressionMethod.Av1 => throw new NotSupportedException("AV1 encoding is not implemented."),
HeifCompressionMethod.Hevc => throw new NotSupportedException("HEVC encoding is not implemented."),
_ => throw new NotSupportedException($"HEIF compression method '{this.encoder.CompressionMethod}' is not supported.")
};
@ -139,15 +138,11 @@ internal sealed class HeifEncoderCore
/// <param name="stream">The destination stream.</param>
private void WriteFileTypeBox(Stream stream)
{
Span<byte> buffer = stackalloc byte[24];
Span<byte> buffer = stackalloc byte[16];
int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Ftyp);
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Heic);
bytesWritten += 4;
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], 0);
bytesWritten += 4;
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Mif1);
bytesWritten += 4;
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Heic);
BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], 0);
bytesWritten += 4;
BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten);
@ -455,7 +450,7 @@ internal sealed class HeifEncoderCore
if (this.encoder.Quality == 0)
{
// Zero is meaningful to the AV1 and HEVC quality scales, but ImageSharp's JPEG encoder deliberately
// Zero is meaningful to the AV1 quality scale, but ImageSharp's JPEG encoder deliberately
// exposes the JPEG quality scale as 1 through 100. Reject the codec-specific mismatch at this boundary.
throw new NotSupportedException("Legacy JPEG image items support quality values in the range [1..100].");
}

2
src/ImageSharp/Formats/Heif/HeifFileType.cs

@ -19,7 +19,7 @@ internal enum HeifFileType
StillImage,
/// <summary>
/// The container presents a timed HEIC or AVIF image sequence.
/// The container presents a timed AVIF image sequence.
/// </summary>
ImageSequence
}

7
src/ImageSharp/Formats/Heif/HeifItem.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Hevc;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
@ -127,12 +126,6 @@ internal class HeifItem(Heif4CharCode type, uint id)
/// </summary>
public Av1LayeredImageIndex? Av1LayeredImageIndex { get; set; }
/// <summary>
/// Gets or sets the HEVC codec configuration associated with this coded image item, or <see langword="null"/>
/// when the item has no HEVC codec-configuration property.
/// </summary>
public HevcCodecConfiguration? HevcCodecConfiguration { get; set; }
/// <summary>
/// Gets or sets the relative pixel spacing associated with this image item, or <see langword="null"/> when the
/// item has no pixel-aspect-ratio property.

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

@ -4,7 +4,6 @@
using System.Buffers;
using System.Buffers.Binary;
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Hevc;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
@ -847,30 +846,10 @@ internal sealed class HeifSequenceParser
this.ParseDirectReferences(stream, sampleGroupDescriptions, sampleToGroup, track, scratch);
}
if (compositionOffsets.IsPresent)
if (compositionOffsets.IsPresent || compositionToDecode.IsPresent)
{
if (track.CodecType == Heif4CharCode.Av01)
{
// AV1-ISOBMFF defines AV1 sample composition time as decode time and explicitly prohibits ctts.
throw new InvalidImageContentException("An AV1 image-sequence track contains a prohibited composition-offset box.");
}
stream.Position = compositionOffsets.Offset;
CompositionSummary composition = ParseCompositionOffsets(stream, compositionOffsets.Length, track, scratch);
if (composition.HasHiddenSamples && (!compositionToDecode.IsPresent || !track.HasEditList))
{
throw new InvalidImageContentException("A HEVC image-sequence track has hidden samples without the required composition and edit boxes.");
}
if (compositionToDecode.IsPresent)
{
stream.Position = compositionToDecode.Offset;
ParseCompositionToDecode(stream, compositionToDecode.Length, composition, scratch);
}
}
else if (compositionToDecode.IsPresent)
{
throw new InvalidImageContentException("The composition-to-decode box has no composition-offset table.");
// AV1-ISOBMFF defines AV1 sample composition time as decode time and explicitly prohibits both boxes.
throw new InvalidImageContentException("An AV1 image-sequence track contains prohibited composition timing boxes.");
}
SetCompositionTimes(track);
@ -899,7 +878,7 @@ internal sealed class HeifSequenceParser
}
long entryLength = HeifBoxReader.ReadHeader(stream, descriptionEnd, scratch, out Heif4CharCode entryType);
if (entryType is not Heif4CharCode.Av01 and not Heif4CharCode.Hvc1 || entryLength < 78)
if (entryType != Heif4CharCode.Av01 || entryLength < 78)
{
throw new InvalidImageContentException($"The image-sequence sample entry '{entryType}' is unsupported or truncated.");
}
@ -941,19 +920,6 @@ internal sealed class HeifSequenceParser
track.Av1CodecConfiguration = new Av1CodecConfiguration(configuration.GetSpan(), this.options);
}
configurationSeen = true;
break;
case Heif4CharCode.HvcC when entryType == Heif4CharCode.Hvc1:
if (configurationSeen)
{
throw new InvalidImageContentException("The HEVC image-sequence sample entry has duplicate codec configurations.");
}
using (IMemoryOwner<byte> configuration = this.boxReader.ReadPayload(stream, childLength))
{
track.HevcCodecConfiguration = new HevcCodecConfiguration(configuration.GetSpan());
}
configurationSeen = true;
break;
case Heif4CharCode.Ccst:
@ -1232,7 +1198,7 @@ internal sealed class HeifSequenceParser
/// </summary>
/// <param name="stream">The stream positioned at the auxiliary-type payload.</param>
/// <param name="boxLength">The validated auxiliary-type payload length.</param>
/// <returns><see langword="true"/> when the payload contains either registered HEIF alpha URN.</returns>
/// <returns><see langword="true"/> when the payload contains the registered HEIF alpha URN.</returns>
private bool ParseAuxiliaryType(Stream stream, long boxLength)
{
if (boxLength < 5 || boxLength > int.MaxValue)
@ -1250,8 +1216,7 @@ internal sealed class HeifSequenceParser
}
type = type[..^1];
return type.SequenceEqual("urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"u8)
|| type.SequenceEqual("urn:mpeg:hevc:2015:auxid:1"u8);
return type.SequenceEqual("urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"u8);
}
/// <summary>
@ -1990,114 +1955,6 @@ internal sealed class HeifSequenceParser
return (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]);
}
/// <summary>
/// Parses HEVC decode-to-composition offsets and marks non-output reference samples.
/// </summary>
/// <param name="stream">The stream positioned at the composition-offset payload.</param>
/// <param name="boxLength">The validated composition-offset payload length.</param>
/// <param name="track">The selected HEVC track receiving retained composition offsets.</param>
/// <param name="scratch">The parser-owned reusable scratch span.</param>
/// <returns>The complete visible-offset range and hidden-sample state.</returns>
private static CompositionSummary ParseCompositionOffsets(Stream stream, long boxLength, HeifSequenceTrack track, Span<byte> scratch)
{
ReadOnlySpan<byte> prefix = ReadPrefix(stream, boxLength, scratch, 8, "composition offsets");
byte version = prefix[0];
if (version is not 0 and not 1 || ReadFlags(prefix) != 0)
{
throw new InvalidImageContentException("The composition-offset box has an unsupported version or flags.");
}
uint entryCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]);
long entryBytes = checked((long)entryCount * 8);
if (entryCount == 0 || boxLength != 8 + entryBytes)
{
throw new InvalidImageContentException("The composition-offset table is empty or has an invalid length.");
}
HeifBoxPayloadReader reader = new(stream, entryBytes, scratch, "composition offsets");
ulong describedSamples = 0;
int retainedOffset = 0;
long leastOffset = long.MaxValue;
long greatestOffset = long.MinValue;
bool hasHiddenSamples = false;
for (uint entry = 0; entry < entryCount; entry++)
{
uint sampleCount = reader.ReadUInt32();
uint rawOffset = reader.ReadUInt32();
if (sampleCount == 0)
{
throw new InvalidImageContentException("The composition-offset table contains a zero-length run.");
}
bool hidden = version == 1 && rawOffset == 0x80000000;
long compositionOffset = version == 0 ? rawOffset : unchecked((int)rawOffset);
describedSamples = checked(describedSamples + sampleCount);
hasHiddenSamples |= hidden;
if (!hidden)
{
leastOffset = Math.Min(leastOffset, compositionOffset);
greatestOffset = Math.Max(greatestOffset, compositionOffset);
}
int retainedRun = Math.Min((int)Math.Min(sampleCount, int.MaxValue), track.Samples.Length - retainedOffset);
Span<HeifSequenceSample> samples = track.Samples;
for (int i = 0; i < retainedRun; i++)
{
samples[retainedOffset + i].CompositionOffset = compositionOffset;
samples[retainedOffset + i].IsHidden = hidden;
}
retainedOffset += retainedRun;
}
if (describedSamples != track.TotalSampleCount || leastOffset == long.MaxValue)
{
throw new InvalidImageContentException("The composition-offset table does not describe every sample or contains no output sample.");
}
return new CompositionSummary(leastOffset, greatestOffset, hasHiddenSamples);
}
/// <summary>
/// Validates the track-wide composition bounds associated with HEVC non-output and reordered samples.
/// </summary>
/// <param name="stream">The stream positioned at the composition-to-decode payload.</param>
/// <param name="boxLength">The validated composition-to-decode payload length.</param>
/// <param name="composition">The offset range derived from the complete composition-offset table.</param>
/// <param name="scratch">The parser-owned reusable scratch span.</param>
private static void ParseCompositionToDecode(Stream stream, long boxLength, CompositionSummary composition, Span<byte> scratch)
{
ReadOnlySpan<byte> prefix = ReadPrefix(stream, boxLength, scratch, 4, "composition-to-decode");
byte version = prefix[0];
int fieldSize = version switch
{
0 => 4,
1 => 8,
_ => throw new InvalidImageContentException($"The composition-to-decode box has unsupported version {version}.")
};
int requiredLength = 4 + (fieldSize * 5);
prefix = ReadPrefixFromStart(stream, boxLength, scratch, requiredLength, "composition-to-decode");
if (boxLength != requiredLength || ReadFlags(prefix) != 0)
{
throw new InvalidImageContentException("The composition-to-decode box has unsupported flags or length.");
}
long shift = ReadSignedInteger(prefix[4..], fieldSize);
long leastOffset = ReadSignedInteger(prefix[(4 + fieldSize)..], fieldSize);
long greatestOffset = ReadSignedInteger(prefix[(4 + (fieldSize * 2))..], fieldSize);
long compositionStart = ReadSignedInteger(prefix[(4 + (fieldSize * 3))..], fieldSize);
long compositionEnd = ReadSignedInteger(prefix[(4 + (fieldSize * 4))..], fieldSize);
long requiredShift = composition.LeastOffset < 0 ? checked(-composition.LeastOffset) : 0;
if (shift < requiredShift
|| leastOffset != composition.LeastOffset
|| greatestOffset != composition.GreatestOffset
|| (compositionEnd != 0 && compositionEnd < compositionStart))
{
throw new InvalidImageContentException("The composition-to-decode box does not match the track's composition offsets.");
}
}
/// <summary>
/// Computes retained sample composition times while preserving decode-order storage.
/// </summary>
@ -2114,15 +1971,6 @@ internal sealed class HeifSequenceParser
}
}
/// <summary>
/// Reads one signed composition field of the version-selected fixed width.
/// </summary>
/// <param name="data">The field bytes.</param>
/// <param name="fieldSize">The four-byte or eight-byte field width.</param>
/// <returns>The signed field value.</returns>
private static long ReadSignedInteger(ReadOnlySpan<byte> data, int fieldSize)
=> fieldSize == 4 ? BinaryPrimitives.ReadInt32BigEndian(data) : BinaryPrimitives.ReadInt64BigEndian(data);
/// <summary>
/// Parses the single normal-rate edit list used to signal image-sequence repetition.
/// </summary>
@ -2590,38 +2438,4 @@ internal sealed class HeifSequenceParser
/// <returns>A value indicating the relative sort order.</returns>
public int CompareTo(SampleIdIndexEntry other) => this.Id.CompareTo(other.Id);
}
/// <summary>
/// Contains the visible composition-offset range derived from a complete HEVC track.
/// </summary>
private readonly struct CompositionSummary
{
/// <summary>
/// Initializes a new instance of the <see cref="CompositionSummary"/> struct.
/// </summary>
/// <param name="leastOffset">The smallest visible composition offset.</param>
/// <param name="greatestOffset">The greatest visible composition offset.</param>
/// <param name="hasHiddenSamples">Whether the track contains non-output samples.</param>
public CompositionSummary(long leastOffset, long greatestOffset, bool hasHiddenSamples)
{
this.LeastOffset = leastOffset;
this.GreatestOffset = greatestOffset;
this.HasHiddenSamples = hasHiddenSamples;
}
/// <summary>
/// Gets the smallest visible composition offset.
/// </summary>
public long LeastOffset { get; }
/// <summary>
/// Gets the greatest visible composition offset.
/// </summary>
public long GreatestOffset { get; }
/// <summary>
/// Gets a value indicating whether the track contains non-output samples.
/// </summary>
public bool HasHiddenSamples { get; }
}
}

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

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Hevc;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
@ -70,11 +69,6 @@ internal sealed class HeifSequenceTrack
/// </summary>
public Av1CodecConfiguration? Av1CodecConfiguration { get; set; }
/// <summary>
/// Gets or sets the parsed HEVC configuration when <see cref="CodecType"/> is <see cref="Heif4CharCode.Hvc1"/>.
/// </summary>
public HevcCodecConfiguration? HevcCodecConfiguration { get; set; }
/// <summary>
/// Gets or sets the ICC profile associated with the image sequence.
/// </summary>

81
src/ImageSharp/Formats/Heif/Hevc/Color/HevcPlanarSampleBuffer.cs

@ -1,81 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Components;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc.Color;
/// <summary>
/// Adapts reconstructed HEVC planes to the shared HEIF planar color pipeline.
/// </summary>
internal struct HevcPlanarSampleBuffer : IHeifPlanarSampleBuffer<ushort>
{
/// <summary>
/// The reconstructed HEVC picture containing the component planes.
/// </summary>
private readonly HevcPictureBuffer picture;
/// <summary>
/// The progressive-frame 4:2:0 chroma sample location.
/// </summary>
private readonly HevcChromaSampleLocation chromaSampleLocation;
/// <summary>
/// Initializes a new instance of the <see cref="HevcPlanarSampleBuffer"/> struct.
/// </summary>
/// <param name="picture">The reconstructed HEVC picture.</param>
/// <param name="chromaSampleLocation">The progressive-frame 4:2:0 chroma sample location.</param>
public HevcPlanarSampleBuffer(HevcPictureBuffer picture, HevcChromaSampleLocation chromaSampleLocation)
{
this.picture = picture;
this.chromaSampleLocation = chromaSampleLocation;
}
/// <summary>
/// Gets the horizontal offset in half-luma-sample units for each HEVC 4:2:0 chroma-location code.
/// </summary>
private static ReadOnlySpan<byte> ChromaLocationX => [0, 1, 0, 1, 0, 1];
/// <summary>
/// Gets the vertical offset in half-luma-sample units for each HEVC 4:2:0 chroma-location code.
/// </summary>
private static ReadOnlySpan<byte> ChromaLocationY => [1, 1, 0, 0, 2, 2];
/// <inheritdoc/>
public readonly int Width => this.picture.Width;
/// <inheritdoc/>
public readonly int Height => this.picture.Height;
/// <inheritdoc/>
public readonly int LumaBitDepth => this.picture.BitDepthLuma;
/// <inheritdoc/>
public readonly int ChromaBitDepth => this.picture.BitDepthChroma;
/// <inheritdoc/>
public readonly bool IsMonochrome => this.picture.ChromaFormat == 0;
/// <inheritdoc/>
public readonly int ChromaSubsamplingX => this.picture.GetSubsamplingX(HevcPlane.Cb);
/// <inheritdoc/>
public readonly int ChromaSubsamplingY => this.picture.GetSubsamplingY(HevcPlane.Cb);
/// <inheritdoc/>
public readonly int ChromaPositionX
=> this.picture.ChromaFormat == 1 && !this.picture.SeparateColorPlane ? ChromaLocationX[(int)this.chromaSampleLocation] : 0;
/// <inheritdoc/>
public readonly int ChromaPositionY
=> this.picture.ChromaFormat == 1 && !this.picture.SeparateColorPlane ? ChromaLocationY[(int)this.chromaSampleLocation] : 0;
/// <inheritdoc/>
public Span<ushort> GetLumaRowSpan(int row) => this.picture.GetRowSpan(HevcPlane.Y, row);
/// <inheritdoc/>
public Span<ushort> GetChromaBlueRowSpan(int row) => this.picture.GetRowSpan(HevcPlane.Cb, row);
/// <inheritdoc/>
public Span<ushort> GetChromaRedRowSpan(int row) => this.picture.GetRowSpan(HevcPlane.Cr, row);
}

179
src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuvConverter.cs

@ -1,179 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Components;
using SixLabors.ImageSharp.Formats.Heif.Components.Alpha;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc.Color;
/// <summary>
/// Adapts HEVC color signaling and reconstructed planes to the shared HEIF color pipeline.
/// </summary>
internal static class HevcYuvConverter
{
/// <summary>
/// Converts reconstructed HEVC component planes to packed pixels.
/// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
/// <param name="configuration">The configuration used for allocation and pixel conversion.</param>
/// <param name="picture">The reconstructed HEVC picture.</param>
/// <param name="image">The destination image frame.</param>
/// <param name="colorProfile">The effective H.273 color description.</param>
/// <param name="chromaSampleLocation">The progressive-frame 4:2:0 chroma sample location.</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>
public static void ConvertToRgb<TPixel>(
Configuration configuration,
HevcPictureBuffer picture,
ImageFrame<TPixel> image,
CicpProfile colorProfile,
HevcChromaSampleLocation chromaSampleLocation,
int sourceX = 0,
int sourceY = 0)
where TPixel : unmanaged, IPixel<TPixel>
{
HeifColorConversionParameters parameters = GetConversionParameters(picture, colorProfile, out HeifColorConversionMode mode);
HevcPlanarSampleBuffer buffer = new(picture, chromaSampleLocation);
if (HeifYuvToRgb8Converter.SupportsLibheifConversion(
buffer.ChromaSubsamplingX,
buffer.ChromaSubsamplingY,
buffer.LumaBitDepth,
buffer.ChromaBitDepth,
buffer.IsMonochrome,
mode))
{
// libheif 1.23.1 is the pinned HEIC presentation implementation. Its pipeline search selects the
// lower-cost direct YCbCr operation when preferred-only upsampling is disabled, so subsampled chroma is
// nearest-replicated and high-bit-depth RGB is rounded before a separate shift to eight bits.
HeifYuvToRgb8Converter.ConvertLibheif(
configuration,
buffer,
image,
in parameters,
sourceX,
sourceY);
return;
}
if (HeifYuvToRgb16Converter.SupportsLibheifConversion(
buffer.ChromaSubsamplingX,
buffer.ChromaSubsamplingY,
buffer.LumaBitDepth,
buffer.ChromaBitDepth,
buffer.IsMonochrome,
mode))
{
// High-bit-depth conversion retains every rounded source-precision RGB bit in UInt16 storage before
// PixelOperations performs the requested TPixel conversion. This prevents an Rgba32 test from concealing
// precision loss in Rgba64, Rgb48, or floating-point decoder output.
HeifYuvToRgb16Converter.Convert(
configuration,
buffer,
image,
in parameters,
sourceX,
sourceY);
return;
}
HeifPlanarColorConverter.ConvertToRgb<TPixel, HevcPlanarSampleBuffer>(
configuration,
buffer,
image,
in parameters,
mode,
sourceX,
sourceY);
}
/// <summary>
/// Composes a visible HEVC luma rectangle into a packed color frame as auxiliary alpha.
/// </summary>
/// <typeparam name="TPixel">The destination color pixel type.</typeparam>
/// <param name="configuration">The configuration used for allocation and pixel conversion.</param>
/// <param name="picture">The reconstructed HEVC picture containing the alpha luma plane.</param>
/// <param name="destination">The packed color frame receiving alpha values.</param>
/// <param name="colorProfile">The effective H.273 color description defining the luma range.</param>
/// <param name="chromaSampleLocation">The progressive-frame 4:2:0 chroma sample location.</param>
/// <param name="sourceRectangle">The visible luma rectangle within the coded picture.</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="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
public static void ComposeAlpha<TPixel>(
Configuration configuration,
HevcPictureBuffer picture,
ImageFrame<TPixel> destination,
CicpProfile colorProfile,
HevcChromaSampleLocation chromaSampleLocation,
Rectangle sourceRectangle,
Size outputSize,
Rectangle destinationRectangle,
bool premultiplied)
where TPixel : unmanaged, IPixel<TPixel>
{
HeifColorConversionParameters parameters = GetConversionParameters(picture, colorProfile, out _);
HevcPlanarSampleBuffer buffer = new(picture, chromaSampleLocation);
HeifPlanarAlphaCompositor.Compose<TPixel, HevcPlanarSampleBuffer, ushort, HeifUShortSampleConverter>(
configuration,
buffer,
destination,
in parameters,
sourceRectangle,
outputSize,
destinationRectangle,
premultiplied);
}
/// <summary>
/// Converts packed pixels to the configured HEVC component planes.
/// </summary>
/// <typeparam name="TPixel">The source pixel type.</typeparam>
/// <param name="configuration">The configuration used for allocation and pixel conversion.</param>
/// <param name="image">The source image frame.</param>
/// <param name="picture">The destination HEVC picture.</param>
/// <param name="colorProfile">The H.273 color description to encode.</param>
/// <param name="chromaSampleLocation">The progressive-frame 4:2:0 chroma sample location.</param>
public static void ConvertFromRgb<TPixel>(
Configuration configuration,
ImageFrame<TPixel> image,
HevcPictureBuffer picture,
CicpProfile colorProfile,
HevcChromaSampleLocation chromaSampleLocation)
where TPixel : unmanaged, IPixel<TPixel>
{
HeifColorConversionParameters parameters = GetConversionParameters(picture, colorProfile, out HeifColorConversionMode mode);
HevcPlanarSampleBuffer buffer = new(picture, chromaSampleLocation);
HeifPlanarColorConverter.ConvertFromRgb<TPixel, HevcPlanarSampleBuffer, ushort, HeifUShortSampleConverter>(
configuration,
image,
buffer,
in parameters,
mode);
}
/// <summary>
/// Resolves the shared H.273 conversion parameters for an HEVC picture.
/// </summary>
/// <param name="picture">The picture defining component precision and sampling.</param>
/// <param name="colorProfile">The effective H.273 color description.</param>
/// <param name="mode">The resolved color conversion operation.</param>
/// <returns>The immutable scalar and SIMD conversion parameters.</returns>
private static HeifColorConversionParameters GetConversionParameters(
HevcPictureBuffer picture,
CicpProfile colorProfile,
out HeifColorConversionMode mode)
=> HeifColorConversionParameters.Create(
colorProfile.ColorPrimaries,
colorProfile.TransferCharacteristics,
colorProfile.MatrixCoefficients,
colorProfile.FullRange,
picture.BitDepthLuma,
picture.ChromaFormat == 0 ? picture.BitDepthLuma : picture.BitDepthChroma,
picture.ChromaFormat == 0,
picture.ChromaFormat == 3,
out mode);
}

230
src/ImageSharp/Formats/Heif/Hevc/HevcBitReader.cs

@ -1,230 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Reads fixed-width and Exp-Golomb HEVC syntax from a most-significant-bit-first byte span.
/// </summary>
internal ref struct HevcBitReader
{
/// <summary>
/// The complete raw byte sequence buffer.
/// </summary>
private readonly ReadOnlySpan<byte> data;
/// <summary>
/// The zero-based position of the next bit to read.
/// </summary>
private int bitPosition;
/// <summary>
/// Initializes a new instance of the <see cref="HevcBitReader"/> struct.
/// </summary>
/// <param name="data">The bounded HEVC syntax bytes.</param>
public HevcBitReader(ReadOnlySpan<byte> data)
{
this.data = data;
this.bitPosition = 0;
}
/// <summary>
/// Gets the zero-based position of the next bit to read.
/// </summary>
public readonly int BitPosition => this.bitPosition;
/// <summary>
/// Gets the number of unread bits in the bounded byte span.
/// </summary>
public readonly int BitsRemaining => (this.data.Length * 8) - this.bitPosition;
/// <summary>
/// Gets a value indicating whether the next bit begins a byte.
/// </summary>
public readonly bool IsByteAligned => (this.bitPosition & 7) == 0;
/// <summary>
/// Reads an unsigned fixed-width value in most-significant-bit-first order.
/// </summary>
/// <param name="bitCount">The number of bits to read.</param>
/// <returns>The decoded unsigned value.</returns>
/// <exception cref="InvalidImageContentException">
/// The requested value extends beyond the bounded HEVC syntax.
/// </exception>
public uint ReadBits(int bitCount)
{
DebugGuard.MustBeBetweenOrEqualTo(bitCount, 0, 32, nameof(bitCount));
if (bitCount > this.BitsRemaining)
{
throw new InvalidImageContentException("The HEVC bitstream is truncated.");
}
uint value = 0;
int remaining = bitCount;
while (remaining > 0)
{
// HEVC fixed-width syntax is MSB-first. Reading only the available portion of each byte keeps the
// same operation valid for both aligned parameter fields and fields that straddle byte boundaries.
int byteOffset = this.bitPosition >> 3;
int bitOffset = this.bitPosition & 7;
int bitsFromByte = Math.Min(remaining, 8 - bitOffset);
int shift = 8 - bitOffset - bitsFromByte;
uint mask = (1U << bitsFromByte) - 1;
value = (value << bitsFromByte) | ((uint)(this.data[byteOffset] >> shift) & mask);
this.bitPosition += bitsFromByte;
remaining -= bitsFromByte;
}
return value;
}
/// <summary>
/// Reads a one-bit HEVC flag.
/// </summary>
/// <returns><see langword="true"/> when the coded flag is one; otherwise, <see langword="false"/>.</returns>
/// <exception cref="InvalidImageContentException">The flag extends beyond the bounded HEVC syntax.</exception>
public bool ReadFlag() => this.ReadBits(1) != 0;
/// <summary>
/// Determines whether unread syntax remains before the raw byte sequence payload trailing bits.
/// </summary>
/// <returns>
/// <see langword="true"/> when the unread bits contain syntax before the stop bit; otherwise,
/// <see langword="false"/>.
/// </returns>
public bool HasMoreRbspData()
{
int bitsRemaining = this.BitsRemaining;
if (bitsRemaining == 0)
{
return false;
}
if (bitsRemaining > 8)
{
return true;
}
int savedBitPosition = this.bitPosition;
uint remainingValue = this.ReadBits(bitsRemaining);
this.bitPosition = savedBitPosition;
// At most one partial byte can contain only rbsp_stop_one_bit followed by alignment zeros.
return remainingValue != 1U << (bitsRemaining - 1);
}
/// <summary>
/// Reads an unsigned exponential-Golomb value.
/// </summary>
/// <returns>The decoded unsigned value.</returns>
/// <exception cref="InvalidImageContentException">
/// The code is truncated or exceeds the range of a 32-bit unsigned integer.
/// </exception>
public uint ReadUnsignedExpGolomb()
{
int leadingZeroBits = 0;
while (!this.ReadFlag())
{
leadingZeroBits++;
if (leadingZeroBits > 32)
{
throw new InvalidImageContentException("The HEVC unsigned Exp-Golomb value exceeds 32 bits.");
}
}
// In ue(v), the zero-prefix length selects an all-one basis and the equally wide suffix selects the
// offset from that basis. Keeping those parts separate makes the 32-bit overflow boundary explicit.
uint suffix = this.ReadBits(leadingZeroBits);
if (leadingZeroBits == 32)
{
// Only an all-zero suffix fits after the 32-bit all-one basis.
if (suffix != 0)
{
throw new InvalidImageContentException("The HEVC unsigned Exp-Golomb value exceeds 32 bits.");
}
return uint.MaxValue;
}
return ((1U << leadingZeroBits) - 1) + suffix;
}
/// <summary>
/// Reads a signed exponential-Golomb value.
/// </summary>
/// <returns>The decoded signed value.</returns>
/// <exception cref="InvalidImageContentException">
/// The code is truncated or exceeds the range of a 32-bit signed integer.
/// </exception>
public int ReadSignedExpGolomb()
{
uint codeNumber = this.ReadUnsignedExpGolomb();
// HEVC's se(v) mapping alternates positive and negative magnitudes: 0, 1, -1, 2, -2, and so on.
if ((codeNumber & 1) == 0)
{
return -(int)(codeNumber >> 1);
}
ulong magnitude = ((ulong)codeNumber + 1) >> 1;
if (magnitude > int.MaxValue)
{
throw new InvalidImageContentException("The HEVC signed Exp-Golomb value exceeds 32 bits.");
}
return (int)magnitude;
}
/// <summary>
/// Reads the one-bit marker and zero padding that align slice data to the next byte boundary.
/// </summary>
/// <exception cref="InvalidImageContentException">
/// The alignment marker is zero or any following alignment bit is nonzero.
/// </exception>
public void ReadByteAlignment()
{
if (!this.ReadFlag())
{
throw new InvalidImageContentException("The HEVC slice-header alignment marker is not set.");
}
while (!this.IsByteAligned)
{
if (this.ReadFlag())
{
throw new InvalidImageContentException("The HEVC slice header has a nonzero alignment bit.");
}
}
}
/// <summary>
/// Reads and validates the stop bit and zero alignment bits that terminate an HEVC raw byte sequence payload.
/// </summary>
/// <exception cref="InvalidImageContentException">
/// The trailing-bit pattern is truncated, malformed, or followed by additional data.
/// </exception>
public void ReadRbspTrailingBits()
{
// An RBSP ends with one stop bit followed only by zero bits up to the next byte boundary.
if (!this.ReadFlag())
{
throw new InvalidImageContentException("The HEVC RBSP stop bit is not set.");
}
while (!this.IsByteAligned)
{
if (this.ReadFlag())
{
throw new InvalidImageContentException("The HEVC RBSP has a nonzero alignment bit.");
}
}
// Each reader is bounded to one RBSP, so reaching alignment before the buffer end means the caller left
// syntax unread or the NAL unit contains bytes beyond its normative terminator.
if (this.BitsRemaining != 0)
{
throw new InvalidImageContentException("The HEVC RBSP contains unexpected trailing data.");
}
}
}

88
src/ImageSharp/Formats/Heif/Hevc/HevcCabacContext.cs

@ -1,88 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Maintains the adaptive probability state for one HEVC context-coded binary syntax element.
/// </summary>
internal struct HevcCabacContext
{
/// <summary>
/// The packed probability-state index and most-probable-symbol value.
/// </summary>
private byte state;
/// <summary>
/// Initializes a new instance of the <see cref="HevcCabacContext"/> struct.
/// </summary>
/// <param name="quantizationParameter">The luma quantization parameter that selects the initial probability.</param>
/// <param name="initializationValue">The syntax-element initialization value.</param>
public HevcCabacContext(int quantizationParameter, byte initializationValue)
{
int clippedQuantizationParameter = Math.Clamp(quantizationParameter, 0, 51);
int slope = ((initializationValue >> 4) * 5) - 45;
int offset = ((initializationValue & 15) << 3) - 16;
int initializationState = Math.Clamp(
((slope * clippedQuantizationParameter) >> 4) + offset,
1,
126);
bool mostProbableSymbol = initializationState >= 64;
this.state = (byte)(
((mostProbableSymbol ? initializationState - 64 : 63 - initializationState) << 1)
+ (mostProbableSymbol ? 1 : 0));
}
/// <summary>
/// Gets a mapping from each packed context state to the state that follows its most-probable symbol.
/// </summary>
// ReadOnlySpan allows the compiler to embed both normative tables in static data instead of allocating
// mutable arrays when this type is initialized.
private static ReadOnlySpan<byte> MostProbableStateTransitions =>
[
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,
34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65,
66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81,
82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97,
98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113,
114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 124, 125, 126, 127
];
/// <summary>
/// Gets a mapping from each packed context state to the state that follows its least-probable symbol.
/// </summary>
private static ReadOnlySpan<byte> LeastProbableStateTransitions =>
[
1, 0, 0, 1, 2, 3, 4, 5, 4, 5, 8, 9, 8, 9, 10, 11,
12, 13, 14, 15, 16, 17, 18, 19, 18, 19, 22, 23, 22, 23, 24, 25,
26, 27, 26, 27, 30, 31, 30, 31, 32, 33, 32, 33, 36, 37, 36, 37,
38, 39, 38, 39, 42, 43, 42, 43, 44, 45, 44, 45, 46, 47, 48, 49,
48, 49, 50, 51, 52, 53, 52, 53, 54, 55, 54, 55, 56, 57, 58, 59,
58, 59, 60, 61, 60, 61, 60, 61, 62, 63, 64, 65, 64, 65, 66, 67,
66, 67, 66, 67, 68, 69, 68, 69, 70, 71, 70, 71, 70, 71, 72, 73,
72, 73, 72, 73, 74, 75, 74, 75, 74, 75, 76, 77, 76, 77, 126, 127
];
/// <summary>
/// Gets the probability-state index used to select the least-probable-symbol range.
/// </summary>
public readonly int StateIndex => this.state >> 1;
/// <summary>
/// Gets a value indicating whether one is the current most-probable symbol.
/// </summary>
public readonly bool MostProbableSymbol => (this.state & 1) != 0;
/// <summary>
/// Advances the context after decoding its most-probable symbol.
/// </summary>
public void UpdateMostProbableSymbol() => this.state = MostProbableStateTransitions[this.state];
/// <summary>
/// Advances the context after decoding its least-probable symbol.
/// </summary>
public void UpdateLeastProbableSymbol() => this.state = LeastProbableStateTransitions[this.state];
}

319
src/ImageSharp/Formats/Heif/Hevc/HevcCabacContexts.cs

@ -1,319 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Owns the adaptive CABAC probability contexts used to decode one intra-coded HEVC entropy substream.
/// </summary>
internal sealed class HevcCabacContexts
{
/// <summary>
/// The first transquant-bypass context.
/// </summary>
private const int TransquantBypassOffset = 0;
/// <summary>
/// The first coding-unit split context.
/// </summary>
private const int SplitOffset = 1;
/// <summary>
/// The intra partition-size context.
/// </summary>
private const int PartitionSizeOffset = 4;
/// <summary>
/// The luma intra-prediction context.
/// </summary>
private const int IntraPredictionOffset = 5;
/// <summary>
/// The first chroma intra-prediction context.
/// </summary>
private const int ChromaPredictionOffset = 6;
/// <summary>
/// The first luma quantization-delta context.
/// </summary>
private const int DeltaQuantizationOffset = 8;
/// <summary>
/// The chroma quantization-adjustment flag context.
/// </summary>
private const int ChromaQuantizationAdjustmentFlagOffset = 11;
/// <summary>
/// The chroma quantization-adjustment index context.
/// </summary>
private const int ChromaQuantizationAdjustmentIndexOffset = 12;
/// <summary>
/// The first transform-tree coded-block-flag context.
/// </summary>
private const int TransformCodedBlockFlagOffset = 13;
/// <summary>
/// The first horizontal last-significant-coefficient context.
/// </summary>
private const int LastSignificantXOffset = 23;
/// <summary>
/// The first vertical last-significant-coefficient context.
/// </summary>
private const int LastSignificantYOffset = 53;
/// <summary>
/// The first significant-coefficient-group context.
/// </summary>
private const int SignificantCoefficientGroupOffset = 83;
/// <summary>
/// The first significant-coefficient context.
/// </summary>
private const int SignificantCoefficientOffset = 87;
/// <summary>
/// The first greater-than-one coefficient-level context.
/// </summary>
private const int GreaterThanOneOffset = 131;
/// <summary>
/// The first greater-than-two coefficient-level context.
/// </summary>
private const int GreaterThanTwoOffset = 155;
/// <summary>
/// The sample-adaptive-offset merge context.
/// </summary>
private const int SampleAdaptiveOffsetMergeOffset = 161;
/// <summary>
/// The sample-adaptive-offset type context.
/// </summary>
private const int SampleAdaptiveOffsetTypeOffset = 162;
/// <summary>
/// The first transform-tree subdivision context.
/// </summary>
private const int TransformSubdivisionOffset = 163;
/// <summary>
/// The first transform-skip context.
/// </summary>
private const int TransformSkipOffset = 166;
/// <summary>
/// The first cross-component prediction context.
/// </summary>
private const int CrossComponentPredictionOffset = 168;
/// <summary>
/// The number of contexts used by the independently coded intra-picture syntax.
/// </summary>
public const int ContextCount = 178;
/// <summary>
/// The contiguous adaptive context storage owned by the entropy substream.
/// </summary>
private readonly HevcCabacContext[] contexts;
/// <summary>
/// Initializes a new instance of the <see cref="HevcCabacContexts"/> class for an intra-coded slice.
/// </summary>
/// <param name="quantizationParameter">The slice luma quantization parameter.</param>
public HevcCabacContexts(int quantizationParameter)
{
this.contexts = new HevcCabacContext[ContextCount];
for (int index = 0; index < this.contexts.Length; index++)
{
this.contexts[index] = new HevcCabacContext(quantizationParameter, IntraInitializationValues[index]);
}
}
/// <summary>
/// Gets the HEVC intra-slice initialization values in the same order as the owned context ranges.
/// </summary>
private static ReadOnlySpan<byte> IntraInitializationValues =>
[
// cu_transquant_bypass_flag
154,
// split_cu_flag
139, 141, 157,
// part_mode and prev_intra_luma_pred_flag
184,
184,
// intra_chroma_pred_mode
63, 139,
// cu_qp_delta_abs, cu_chroma_qp_offset_flag, and cu_chroma_qp_offset_idx
154, 154, 154,
154,
154,
// cbf_luma followed by the chroma coded-block flags
111, 141, 154, 154, 154,
94, 138, 182, 154, 154,
// last_sig_coeff_x_prefix: luma followed by chroma
110, 110, 124, 125, 140, 153, 125, 127, 140, 109, 111, 143, 127, 111, 79,
108, 123, 63, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154,
// last_sig_coeff_y_prefix: luma followed by chroma
110, 110, 124, 125, 140, 153, 125, 127, 140, 109, 111, 143, 127, 111, 79,
108, 123, 63, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154, 154,
// coded_sub_block_flag: luma followed by chroma
91, 171, 134, 141,
// sig_coeff_flag: luma followed by chroma
111, 111, 125, 110, 110, 94, 124, 108, 124, 107, 125, 141, 179, 153,
125, 107, 125, 141, 179, 153, 125, 107, 125, 141, 179, 153, 125, 141,
140, 139, 182, 182, 152, 136, 152, 136, 153, 136, 139, 111, 136, 139, 111, 111,
// coeff_abs_level_greater1_flag: luma followed by chroma
140, 92, 137, 138, 140, 152, 138, 139, 153, 74, 149, 92, 139, 107, 122, 152,
140, 179, 166, 182, 140, 227, 122, 197,
// coeff_abs_level_greater2_flag: luma followed by chroma
138, 153, 136, 167, 152, 152,
// sao_merge_flag and sao_type_idx
153,
200,
// split_transform_flag
153, 138, 138,
// transform_skip_flag: luma followed by chroma
139, 139,
// cross_comp_pred: five sign/magnitude contexts for Cb followed by five for Cr
154, 154, 154, 154, 154, 154, 154, 154, 154, 154
];
/// <summary>
/// Gets the coding-unit transquant-bypass context.
/// </summary>
public Span<HevcCabacContext> TransquantBypass => this.contexts.AsSpan(TransquantBypassOffset, 1);
/// <summary>
/// Gets the coding-unit split contexts, ordered by neighboring split depth.
/// </summary>
public Span<HevcCabacContext> Split => this.contexts.AsSpan(SplitOffset, 3);
/// <summary>
/// Gets the intra partition-size context.
/// </summary>
public Span<HevcCabacContext> PartitionSize => this.contexts.AsSpan(PartitionSizeOffset, 1);
/// <summary>
/// Gets the luma intra-prediction context.
/// </summary>
public Span<HevcCabacContext> IntraPrediction => this.contexts.AsSpan(IntraPredictionOffset, 1);
/// <summary>
/// Gets the chroma intra-prediction contexts.
/// </summary>
public Span<HevcCabacContext> ChromaPrediction => this.contexts.AsSpan(ChromaPredictionOffset, 2);
/// <summary>
/// Gets the luma quantization-delta contexts.
/// </summary>
public Span<HevcCabacContext> DeltaQuantization => this.contexts.AsSpan(DeltaQuantizationOffset, 3);
/// <summary>
/// Gets the chroma quantization-adjustment flag context.
/// </summary>
public Span<HevcCabacContext> ChromaQuantizationAdjustmentFlag =>
this.contexts.AsSpan(ChromaQuantizationAdjustmentFlagOffset, 1);
/// <summary>
/// Gets the chroma quantization-adjustment index context.
/// </summary>
public Span<HevcCabacContext> ChromaQuantizationAdjustmentIndex =>
this.contexts.AsSpan(ChromaQuantizationAdjustmentIndexOffset, 1);
/// <summary>
/// Gets the transform-tree coded-block-flag contexts, with luma preceding chroma.
/// </summary>
public Span<HevcCabacContext> TransformCodedBlockFlag =>
this.contexts.AsSpan(TransformCodedBlockFlagOffset, 10);
/// <summary>
/// Gets the horizontal last-significant-coefficient contexts, with luma preceding chroma.
/// </summary>
public Span<HevcCabacContext> LastSignificantX => this.contexts.AsSpan(LastSignificantXOffset, 30);
/// <summary>
/// Gets the vertical last-significant-coefficient contexts, with luma preceding chroma.
/// </summary>
public Span<HevcCabacContext> LastSignificantY => this.contexts.AsSpan(LastSignificantYOffset, 30);
/// <summary>
/// Gets the significant-coefficient-group contexts, with luma preceding chroma.
/// </summary>
public Span<HevcCabacContext> SignificantCoefficientGroup =>
this.contexts.AsSpan(SignificantCoefficientGroupOffset, 4);
/// <summary>
/// Gets the significant-coefficient contexts, with luma preceding chroma.
/// </summary>
public Span<HevcCabacContext> SignificantCoefficient =>
this.contexts.AsSpan(SignificantCoefficientOffset, 44);
/// <summary>
/// Gets the greater-than-one coefficient-level contexts, with luma preceding chroma.
/// </summary>
public Span<HevcCabacContext> GreaterThanOne => this.contexts.AsSpan(GreaterThanOneOffset, 24);
/// <summary>
/// Gets the greater-than-two coefficient-level contexts, with luma preceding chroma.
/// </summary>
public Span<HevcCabacContext> GreaterThanTwo => this.contexts.AsSpan(GreaterThanTwoOffset, 6);
/// <summary>
/// Gets the sample-adaptive-offset merge context.
/// </summary>
public Span<HevcCabacContext> SampleAdaptiveOffsetMerge =>
this.contexts.AsSpan(SampleAdaptiveOffsetMergeOffset, 1);
/// <summary>
/// Gets the sample-adaptive-offset type context.
/// </summary>
public Span<HevcCabacContext> SampleAdaptiveOffsetType =>
this.contexts.AsSpan(SampleAdaptiveOffsetTypeOffset, 1);
/// <summary>
/// Gets the transform-tree subdivision contexts.
/// </summary>
public Span<HevcCabacContext> TransformSubdivision =>
this.contexts.AsSpan(TransformSubdivisionOffset, 3);
/// <summary>
/// Gets the transform-skip contexts, with luma preceding chroma.
/// </summary>
public Span<HevcCabacContext> TransformSkip => this.contexts.AsSpan(TransformSkipOffset, 2);
/// <summary>
/// Gets the cross-component prediction contexts, with Cb preceding Cr.
/// </summary>
public Span<HevcCabacContext> CrossComponentPrediction =>
this.contexts.AsSpan(CrossComponentPredictionOffset, 10);
/// <summary>
/// Copies every adaptive probability context to caller-owned wavefront state.
/// </summary>
/// <param name="destination">The destination containing at least <see cref="ContextCount"/> elements.</param>
public void CopyTo(Span<HevcCabacContext> destination) => this.contexts.CopyTo(destination);
/// <summary>
/// Restores every adaptive probability context from caller-owned wavefront state.
/// </summary>
/// <param name="source">The source containing at least <see cref="ContextCount"/> elements.</param>
public void CopyFrom(ReadOnlySpan<HevcCabacContext> source) => source[..ContextCount].CopyTo(this.contexts);
}

396
src/ImageSharp/Formats/Heif/Hevc/HevcCabacDecoder.cs

@ -1,396 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Decodes context-adaptive and bypass-coded binary values from one bounded HEVC entropy substream.
/// </summary>
internal ref struct HevcCabacDecoder
{
/// <summary>
/// The complete bounded entropy-substream bytes.
/// </summary>
private readonly ReadOnlySpan<byte> data;
/// <summary>
/// The zero-based offset of the next byte that can refill the arithmetic value register.
/// </summary>
private int byteOffset;
/// <summary>
/// The current arithmetic interval width.
/// </summary>
private uint range;
/// <summary>
/// The current arithmetic code value, scaled by seven fractional bits.
/// </summary>
private uint value;
/// <summary>
/// The number of normalization shifts remaining before the value register requires another byte.
/// </summary>
private int bitsNeeded;
/// <summary>
/// The raw-bit position used while a pulse-code-modulated coding unit suspends arithmetic decoding.
/// </summary>
private int pcmBitOffset;
/// <summary>
/// Initializes a new instance of the <see cref="HevcCabacDecoder"/> struct.
/// </summary>
/// <param name="data">The bytes of one independently bounded HEVC entropy substream.</param>
/// <exception cref="InvalidImageContentException">The entropy substream is shorter than its initial value register.</exception>
public HevcCabacDecoder(ReadOnlySpan<byte> data)
{
if (data.Length < 2)
{
throw new InvalidImageContentException("The HEVC CABAC substream is truncated.");
}
this.data = data;
this.byteOffset = 2;
this.range = 510;
this.value = ((uint)data[0] << 8) | data[1];
this.bitsNeeded = -8;
this.pcmBitOffset = 0;
}
/// <summary>
/// Gets the least-probable-symbol subrange for each probability state and current range class.
/// </summary>
private static ReadOnlySpan<byte> LeastProbableSymbolRanges =>
[
128, 176, 208, 240, 128, 167, 197, 227, 128, 158, 187, 216, 123, 150, 178, 205,
116, 142, 169, 195, 111, 135, 160, 185, 105, 128, 152, 175, 100, 122, 144, 166,
95, 116, 137, 158, 90, 110, 130, 150, 85, 104, 123, 142, 81, 99, 117, 135,
77, 94, 111, 128, 73, 89, 105, 122, 69, 85, 100, 116, 66, 80, 95, 110,
62, 76, 90, 104, 59, 72, 86, 99, 56, 69, 81, 94, 53, 65, 77, 89,
51, 62, 73, 85, 48, 59, 69, 80, 46, 56, 66, 76, 43, 53, 63, 72,
41, 50, 59, 69, 39, 48, 56, 65, 37, 45, 54, 62, 35, 43, 51, 59,
33, 41, 48, 56, 32, 39, 46, 53, 30, 37, 43, 50, 29, 35, 41, 48,
27, 33, 39, 45, 26, 31, 37, 43, 24, 30, 35, 41, 23, 28, 33, 39,
22, 27, 32, 37, 21, 26, 30, 35, 20, 24, 29, 33, 19, 23, 27, 31,
18, 22, 26, 30, 17, 21, 25, 28, 16, 20, 23, 27, 15, 19, 22, 25,
14, 18, 21, 24, 14, 17, 20, 23, 13, 16, 19, 22, 12, 15, 18, 21,
12, 14, 17, 20, 11, 14, 16, 19, 11, 13, 15, 18, 10, 12, 15, 17,
10, 12, 14, 16, 9, 11, 13, 15, 9, 11, 12, 14, 8, 10, 12, 14,
8, 9, 11, 13, 7, 9, 11, 12, 7, 9, 10, 12, 7, 8, 10, 11,
6, 8, 9, 11, 6, 7, 9, 10, 6, 7, 8, 9, 2, 2, 2, 2,
];
/// <summary>
/// Gets the normalization shift for each quantized least-probable-symbol range.
/// </summary>
private static ReadOnlySpan<byte> LeastProbableSymbolNormalizationShifts =>
[
6, 5, 4, 4,
3, 3, 3, 3,
2, 2, 2, 2,
2, 2, 2, 2,
1, 1, 1, 1,
1, 1, 1, 1,
1, 1, 1, 1,
1, 1, 1, 1,
];
/// <summary>
/// Gets the number of whole entropy-substream bytes loaded into the arithmetic decoder.
/// </summary>
public readonly int BytesConsumed => this.byteOffset;
/// <summary>
/// Decodes one context-adaptive binary value and advances its probability state.
/// </summary>
/// <param name="context">The adaptive probability context selected for the syntax element.</param>
/// <returns>The decoded binary value.</returns>
/// <exception cref="InvalidImageContentException">The entropy substream ends while normalizing the decoded value.</exception>
public bool ReadDecision(ref HevcCabacContext context)
{
int rangeClass = ((int)this.range >> 6) - 4;
uint leastProbableSymbolRange = LeastProbableSymbolRanges[(context.StateIndex * 4) + rangeClass];
this.range -= leastProbableSymbolRange;
uint scaledRange = this.range << 7;
if (this.value < scaledRange)
{
bool symbol = context.MostProbableSymbol;
context.UpdateMostProbableSymbol();
if (scaledRange < (256U << 7))
{
// Renormalization shifts both registers together so their comparison continues to describe the
// same arithmetic interval; a byte is loaded only when the buffered fractional bits are exhausted.
this.range = scaledRange >> 6;
this.value <<= 1;
if (++this.bitsNeeded == 0)
{
this.bitsNeeded = -8;
this.value += this.ReadByte();
}
}
return symbol;
}
bool leastProbableSymbol = !context.MostProbableSymbol;
int normalizationShift = LeastProbableSymbolNormalizationShifts[(int)(leastProbableSymbolRange >> 3)];
this.value = (this.value - scaledRange) << normalizationShift;
this.range = leastProbableSymbolRange << normalizationShift;
context.UpdateLeastProbableSymbol();
this.bitsNeeded += normalizationShift;
if (this.bitsNeeded >= 0)
{
this.value += (uint)this.ReadByte() << this.bitsNeeded;
this.bitsNeeded -= 8;
}
return leastProbableSymbol;
}
/// <summary>
/// Decodes one equal-probability binary value without changing an adaptive context.
/// </summary>
/// <returns>The decoded binary value.</returns>
/// <exception cref="InvalidImageContentException">The entropy substream ends while loading the decoded value.</exception>
public bool ReadBypass()
{
if (this.range == 256)
{
return this.ReadAlignedBypassBits(1) != 0;
}
this.value <<= 1;
if (++this.bitsNeeded >= 0)
{
this.bitsNeeded = -8;
this.value += this.ReadByte();
}
uint scaledRange = this.range << 7;
if (this.value < scaledRange)
{
return false;
}
this.value -= scaledRange;
return true;
}
/// <summary>
/// Decodes a most-significant-bit-first sequence of equal-probability binary values.
/// </summary>
/// <param name="bitCount">The number of values to decode.</param>
/// <returns>The decoded unsigned value.</returns>
/// <exception cref="InvalidImageContentException">The entropy substream ends while loading the decoded value.</exception>
public uint ReadBypassBits(int bitCount)
{
DebugGuard.MustBeBetweenOrEqualTo(bitCount, 0, 32, nameof(bitCount));
if (this.range == 256)
{
return this.ReadAlignedBypassBits(bitCount);
}
uint bins = 0;
int remaining = bitCount;
while (remaining > 8)
{
this.value = (this.value << 8) + ((uint)this.ReadByte() << (8 + this.bitsNeeded));
uint scaledRange = this.range << 15;
for (int bitIndex = 0; bitIndex < 8; bitIndex++)
{
bins <<= 1;
scaledRange >>= 1;
if (this.value >= scaledRange)
{
bins++;
this.value -= scaledRange;
}
}
remaining -= 8;
}
this.bitsNeeded += remaining;
this.value <<= remaining;
if (this.bitsNeeded >= 0)
{
this.value += (uint)this.ReadByte() << this.bitsNeeded;
this.bitsNeeded -= 8;
}
uint finalScaledRange = this.range << (remaining + 7);
for (int bitIndex = 0; bitIndex < remaining; bitIndex++)
{
bins <<= 1;
finalScaledRange >>= 1;
if (this.value >= finalScaledRange)
{
bins++;
this.value -= finalScaledRange;
}
}
return bins;
}
/// <summary>
/// Selects the byte-aligned equal-probability range used by aligned bypass syntax.
/// </summary>
public void AlignBypass() => this.range = 256;
/// <summary>
/// Decodes the binary value that terminates a coding-tree block or entropy substream.
/// </summary>
/// <returns><see langword="true"/> when the current entropy substream terminates; otherwise, <see langword="false"/>.</returns>
/// <exception cref="InvalidImageContentException">The entropy substream ends while normalizing a non-terminating value.</exception>
public bool ReadTerminate()
{
this.range -= 2;
uint scaledRange = this.range << 7;
if (this.value >= scaledRange)
{
return true;
}
if (scaledRange < (256U << 7))
{
this.range = scaledRange >> 6;
this.value <<= 1;
if (++this.bitsNeeded == 0)
{
this.bitsNeeded = -8;
this.value += this.ReadByte();
}
}
return false;
}
/// <summary>
/// Decodes the terminating-bin flag that enters pulse-code-modulated sample syntax.
/// </summary>
/// <returns><see langword="true"/> when raw PCM samples follow; otherwise, <see langword="false"/>.</returns>
public bool ReadPcmFlag()
{
bool pcm = this.ReadTerminate();
if (pcm)
{
// A successful terminating bin leaves the underlying byte reader at the first byte after the CABAC
// alignment pattern. PCM sample bits start there and temporarily bypass the arithmetic registers.
this.pcmBitOffset = this.byteOffset * 8;
}
return pcm;
}
/// <summary>
/// Reads one unsigned pulse-code-modulated sample while arithmetic decoding is suspended.
/// </summary>
/// <param name="bitDepth">The number of most-significant-bit-first sample bits.</param>
/// <returns>The decoded sample value.</returns>
/// <exception cref="InvalidImageContentException">The entropy substream ends within the PCM sample.</exception>
public ushort ReadPcmSample(int bitDepth)
{
DebugGuard.MustBeBetweenOrEqualTo(bitDepth, 1, 16, nameof(bitDepth));
if (this.pcmBitOffset > (this.data.Length * 8) - bitDepth)
{
throw new InvalidImageContentException("The HEVC pulse-code-modulated sample data is truncated.");
}
uint sample = 0;
int bitsRemaining = bitDepth;
while (bitsRemaining > 0)
{
int byteIndex = this.pcmBitOffset >> 3;
int bitIndex = this.pcmBitOffset & 7;
int bitsFromByte = Math.Min(8 - bitIndex, bitsRemaining);
int shift = 8 - bitIndex - bitsFromByte;
uint mask = (uint)((1 << bitsFromByte) - 1);
sample = (sample << bitsFromByte) | ((uint)(this.data[byteIndex] >> shift) & mask);
this.pcmBitOffset += bitsFromByte;
bitsRemaining -= bitsFromByte;
}
return (ushort)sample;
}
/// <summary>
/// Restarts arithmetic decoding after a complete byte-aligned PCM coding unit.
/// </summary>
/// <exception cref="InvalidImageContentException">The following arithmetic substream is truncated.</exception>
public void RestartAfterPcm()
{
DebugGuard.IsTrue((this.pcmBitOffset & 7) == 0, "The complete HEVC PCM payload must end on a byte boundary.");
this.byteOffset = this.pcmBitOffset >> 3;
this.range = 510;
this.bitsNeeded = -8;
this.value = ((uint)this.ReadByte() << 8) | this.ReadByte();
}
/// <summary>
/// Validates the stop bit and zero padding following a terminating entropy-coded value.
/// </summary>
/// <exception cref="InvalidImageContentException">The entropy substream has an invalid stop or alignment bit.</exception>
public readonly void ValidateTerminationAlignment()
{
int alignmentShift = 8 + this.bitsNeeded;
// CABAC refills whole bytes ahead of consumption. The stop bit therefore remains in the most recently
// loaded byte, and bitsNeeded identifies its exact position without rewinding the arithmetic decoder.
int alignmentPattern = (this.data[this.byteOffset - 1] << alignmentShift) & 0xFF;
if (alignmentPattern != 0x80)
{
throw new InvalidImageContentException("The HEVC CABAC substream has invalid termination alignment.");
}
}
/// <summary>
/// Decodes equal-probability values while the arithmetic range is byte aligned.
/// </summary>
/// <param name="bitCount">The number of values to decode.</param>
/// <returns>The decoded unsigned value.</returns>
/// <exception cref="InvalidImageContentException">The entropy substream ends while loading the decoded value.</exception>
private uint ReadAlignedBypassBits(int bitCount)
{
uint bins = 0;
int remaining = bitCount;
while (remaining > 0)
{
int binsToRead = Math.Min(remaining, 8);
uint binMask = (1U << binsToRead) - 1;
// With a range of 256 the high value bit is known to be zero, so the following bits can be copied
// directly while preserving the same register refill schedule as individual bypass decisions.
uint newBins = (this.value >> (15 - binsToRead)) & binMask;
bins = (bins << binsToRead) | newBins;
this.value = (this.value << binsToRead) & 0x7FFF;
remaining -= binsToRead;
this.bitsNeeded += binsToRead;
if (this.bitsNeeded >= 0)
{
this.value |= (uint)this.ReadByte() << this.bitsNeeded;
this.bitsNeeded -= 8;
}
}
return bins;
}
/// <summary>
/// Loads the next byte into the arithmetic decoder.
/// </summary>
/// <returns>The next entropy-substream byte.</returns>
/// <exception cref="InvalidImageContentException">No byte remains in the bounded entropy substream.</exception>
private byte ReadByte()
{
if ((uint)this.byteOffset >= (uint)this.data.Length)
{
throw new InvalidImageContentException("The HEVC CABAC substream is truncated.");
}
return this.data[this.byteOffset++];
}
}

575
src/ImageSharp/Formats/Heif/Hevc/HevcCabacSyntaxReader.cs

@ -1,575 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Decodes the CABAC syntax values used to reconstruct one independently coded HEVC still picture.
/// </summary>
internal ref struct HevcCabacSyntaxReader
{
/// <summary>
/// The truncated-unary cutoff for a coding-unit luma quantization delta.
/// </summary>
private const int DeltaQuantizationCutoff = 5;
/// <summary>
/// The prefix length at which coefficient levels switch from Rice to exponential-Golomb coding.
/// </summary>
private const int CoefficientRemainingReduction = 3;
/// <summary>
/// The binary arithmetic decoder for the current entropy substream.
/// </summary>
private HevcCabacDecoder decoder;
/// <summary>
/// The adaptive intra-picture probability contexts for the current entropy substream.
/// </summary>
private readonly HevcCabacContexts contexts;
/// <summary>
/// Initializes a new instance of the <see cref="HevcCabacSyntaxReader"/> struct.
/// </summary>
/// <param name="data">The bytes of one bounded slice tile or wavefront entropy substream.</param>
/// <param name="quantizationParameter">The slice luma quantization parameter.</param>
/// <exception cref="InvalidImageContentException">The entropy substream is truncated.</exception>
public HevcCabacSyntaxReader(ReadOnlySpan<byte> data, int quantizationParameter)
{
this.decoder = new HevcCabacDecoder(data);
this.contexts = new HevcCabacContexts(quantizationParameter);
}
/// <summary>
/// Gets the number of entropy-substream bytes loaded by the arithmetic decoder.
/// </summary>
public readonly int BytesConsumed => this.decoder.BytesConsumed;
/// <summary>
/// Copies the adaptive contexts required to initialize a later wavefront row.
/// </summary>
/// <param name="destination">The caller-owned context destination.</param>
public readonly void CopyContextsTo(Span<HevcCabacContext> destination) => this.contexts.CopyTo(destination);
/// <summary>
/// Restores adaptive contexts captured after the second coding-tree block of the preceding wavefront row.
/// </summary>
/// <param name="source">The saved wavefront contexts.</param>
public readonly void CopyContextsFrom(ReadOnlySpan<HevcCabacContext> source) => this.contexts.CopyFrom(source);
/// <summary>
/// Decodes the coding-unit transquant-bypass flag.
/// </summary>
/// <returns>The decoded flag value.</returns>
public bool ReadTransquantBypass()
{
Span<HevcCabacContext> selectedContexts = this.contexts.TransquantBypass;
return this.decoder.ReadDecision(ref selectedContexts[0]);
}
/// <summary>
/// Decodes a coding-unit split flag.
/// </summary>
/// <param name="contextIndex">The context derived from the available neighboring coding-unit depths.</param>
/// <returns>The decoded flag value.</returns>
public bool ReadSplit(int contextIndex)
{
DebugGuard.MustBeBetweenOrEqualTo(contextIndex, 0, 2, nameof(contextIndex));
Span<HevcCabacContext> selectedContexts = this.contexts.Split;
return this.decoder.ReadDecision(ref selectedContexts[contextIndex]);
}
/// <summary>
/// Decodes whether a minimum-size intra coding unit uses four square prediction partitions.
/// </summary>
/// <param name="isMinimumCodingBlockSize">
/// A value indicating whether the coding unit is at the minimum coding-block size.
/// </param>
/// <returns>
/// <see langword="true"/> for four square prediction partitions; <see langword="false"/> for one square partition.
/// </returns>
public bool ReadIntraNxNPartition(bool isMinimumCodingBlockSize)
{
if (!isMinimumCodingBlockSize)
{
return false;
}
Span<HevcCabacContext> selectedContexts = this.contexts.PartitionSize;
return !this.decoder.ReadDecision(ref selectedContexts[0]);
}
/// <summary>
/// Decodes whether a square intra coding unit carries raw pulse-code-modulated samples.
/// </summary>
/// <returns><see langword="true"/> when PCM sample syntax follows; otherwise, <see langword="false"/>.</returns>
public bool ReadPcmFlag() => this.decoder.ReadPcmFlag();
/// <summary>
/// Reads one pulse-code-modulated component sample.
/// </summary>
/// <param name="bitDepth">The PCM sample precision.</param>
/// <returns>The decoded unsigned sample.</returns>
public ushort ReadPcmSample(int bitDepth) => this.decoder.ReadPcmSample(bitDepth);
/// <summary>
/// Restarts arithmetic decoding after the complete PCM coding-unit payload.
/// </summary>
public void RestartAfterPcm() => this.decoder.RestartAfterPcm();
/// <summary>
/// Decodes whether a luma intra mode is selected from the three most-probable modes.
/// </summary>
/// <returns>The decoded flag value.</returns>
public bool ReadPreviousIntraLumaPredictionFlag()
{
Span<HevcCabacContext> selectedContexts = this.contexts.IntraPrediction;
return this.decoder.ReadDecision(ref selectedContexts[0]);
}
/// <summary>
/// Decodes the zero-based selector for one of the three most-probable luma intra modes.
/// </summary>
/// <returns>The selector in the inclusive range zero through two.</returns>
public int ReadMostProbableIntraLumaPredictionIndex()
{
if (!this.decoder.ReadBypass())
{
return 0;
}
return this.decoder.ReadBypass() ? 2 : 1;
}
/// <summary>
/// Decodes the five-bit selector for a luma intra mode outside the most-probable set.
/// </summary>
/// <returns>The decoded selector in the inclusive range zero through thirty-one.</returns>
public int ReadRemainingIntraLumaPredictionMode() => (int)this.decoder.ReadBypassBits(5);
/// <summary>
/// Decodes the chroma intra prediction selector.
/// </summary>
/// <returns>
/// Negative one when chroma derives its mode from luma; otherwise, the decoded selector in the inclusive range
/// zero through three.
/// </returns>
public int ReadChromaPredictionModeIndex()
{
Span<HevcCabacContext> selectedContexts = this.contexts.ChromaPrediction;
if (!this.decoder.ReadDecision(ref selectedContexts[0]))
{
return -1;
}
return (int)this.decoder.ReadBypassBits(2);
}
/// <summary>
/// Decodes a transform-tree subdivision flag.
/// </summary>
/// <param name="log2TransformBlockSize">The base-two logarithm of the current transform-block size.</param>
/// <returns>The decoded flag value.</returns>
public bool ReadTransformSubdivision(int log2TransformBlockSize)
{
DebugGuard.MustBeBetweenOrEqualTo(log2TransformBlockSize, 3, 5, nameof(log2TransformBlockSize));
Span<HevcCabacContext> selectedContexts = this.contexts.TransformSubdivision;
return this.decoder.ReadDecision(ref selectedContexts[5 - log2TransformBlockSize]);
}
/// <summary>
/// Decodes a transform-tree coded-block flag.
/// </summary>
/// <param name="isChroma">A value indicating whether the flag describes a chroma transform block.</param>
/// <param name="contextIndex">The transform-depth-derived context index.</param>
/// <returns>The decoded flag value.</returns>
public bool ReadTransformCodedBlockFlag(bool isChroma, int contextIndex)
{
DebugGuard.MustBeBetweenOrEqualTo(contextIndex, 0, 4, nameof(contextIndex));
Span<HevcCabacContext> selectedContexts = this.contexts.TransformCodedBlockFlag;
int channelOffset = isChroma ? 5 : 0;
return this.decoder.ReadDecision(ref selectedContexts[channelOffset + contextIndex]);
}
/// <summary>
/// Decodes whether a transform block bypasses the inverse transform.
/// </summary>
/// <param name="isChroma">A value indicating whether the transform block belongs to a chroma channel.</param>
/// <returns>The decoded flag value.</returns>
public bool ReadTransformSkip(bool isChroma)
{
Span<HevcCabacContext> selectedContexts = this.contexts.TransformSkip;
return this.decoder.ReadDecision(ref selectedContexts[isChroma ? 1 : 0]);
}
/// <summary>
/// Decodes the signed coding-unit luma quantization-parameter delta.
/// </summary>
/// <returns>The signed delta value.</returns>
/// <exception cref="InvalidImageContentException">The coded magnitude exceeds a 32-bit signed value.</exception>
public int ReadDeltaQuantizationParameter()
{
Span<HevcCabacContext> selectedContexts = this.contexts.DeltaQuantization;
ulong magnitude = this.ReadTruncatedUnary(selectedContexts, 0, 1, DeltaQuantizationCutoff);
if (magnitude == DeltaQuantizationCutoff)
{
magnitude += this.ReadBypassExponentialGolomb(0);
}
if (magnitude > int.MaxValue)
{
throw new InvalidImageContentException("The HEVC coding-unit quantization delta is too large.");
}
if (magnitude == 0)
{
return 0;
}
int signedMagnitude = (int)magnitude;
return this.decoder.ReadBypass() ? -signedMagnitude : signedMagnitude;
}
/// <summary>
/// Decodes the coding-unit chroma quantization-adjustment selector.
/// </summary>
/// <param name="listLength">The number of chroma offset pairs declared by the picture parameters.</param>
/// <returns>Zero when no adjustment applies; otherwise, the one-based offset-list selector.</returns>
public int ReadChromaQuantizationAdjustment(int listLength)
{
Span<HevcCabacContext> flagContexts = this.contexts.ChromaQuantizationAdjustmentFlag;
if (!this.decoder.ReadDecision(ref flagContexts[0]))
{
return 0;
}
if (listLength == 1)
{
return 1;
}
Span<HevcCabacContext> indexContexts = this.contexts.ChromaQuantizationAdjustmentIndex;
return (int)this.ReadTruncatedUnary(indexContexts, 0, 0, listLength - 1) + 1;
}
/// <summary>
/// Decodes the cross-component residual-prediction scale for one chroma plane.
/// </summary>
/// <param name="chromaPlaneIndex">Zero for Cb or one for Cr.</param>
/// <returns>Zero when prediction is disabled; otherwise, a signed power of two from one through eight.</returns>
public int ReadCrossComponentPredictionScale(int chromaPlaneIndex)
{
DebugGuard.MustBeBetweenOrEqualTo(chromaPlaneIndex, 0, 1, nameof(chromaPlaneIndex));
Span<HevcCabacContext> selectedContexts = this.contexts.CrossComponentPrediction;
int contextOffset = chromaPlaneIndex * 5;
if (!this.decoder.ReadDecision(ref selectedContexts[contextOffset]))
{
return 0;
}
int magnitudeLog2 = 0;
if (this.decoder.ReadDecision(ref selectedContexts[contextOffset + 1]))
{
Span<HevcCabacContext> magnitudeContexts = selectedContexts.Slice(contextOffset + 2, 2);
magnitudeLog2 = (int)this.ReadTruncatedUnary(magnitudeContexts, 0, 1, 2) + 1;
}
int magnitude = 1 << magnitudeLog2;
return this.decoder.ReadDecision(ref selectedContexts[contextOffset + 4]) ? -magnitude : magnitude;
}
/// <summary>
/// Decodes a sample-adaptive-offset merge flag.
/// </summary>
/// <returns>The decoded flag value.</returns>
public bool ReadSampleAdaptiveOffsetMerge()
{
Span<HevcCabacContext> selectedContexts = this.contexts.SampleAdaptiveOffsetMerge;
return this.decoder.ReadDecision(ref selectedContexts[0]);
}
/// <summary>
/// Decodes the sample-adaptive-offset mode selector.
/// </summary>
/// <returns>Zero for off, one for band offset, or two for edge offset.</returns>
public int ReadSampleAdaptiveOffsetType()
{
Span<HevcCabacContext> selectedContexts = this.contexts.SampleAdaptiveOffsetType;
if (!this.decoder.ReadDecision(ref selectedContexts[0]))
{
return 0;
}
return this.decoder.ReadBypass() ? 2 : 1;
}
/// <summary>
/// Decodes a truncated-unary absolute sample-adaptive-offset value.
/// </summary>
/// <param name="maximumValue">The inclusive maximum offset magnitude.</param>
/// <returns>The decoded offset magnitude.</returns>
public int ReadSampleAdaptiveOffsetAbsolute(int maximumValue)
{
if (maximumValue == 0 || !this.decoder.ReadBypass())
{
return 0;
}
int value = 1;
while (value < maximumValue && this.decoder.ReadBypass())
{
value++;
}
return value;
}
/// <summary>
/// Decodes the five-bit sample-adaptive band-offset starting position.
/// </summary>
/// <returns>The decoded band position.</returns>
public int ReadSampleAdaptiveOffsetBandPosition() => (int)this.decoder.ReadBypassBits(5);
/// <summary>
/// Decodes the two-bit sample-adaptive edge-offset class.
/// </summary>
/// <returns>The decoded edge class.</returns>
public int ReadSampleAdaptiveOffsetEdgeClass() => (int)this.decoder.ReadBypassBits(2);
/// <summary>
/// Decodes a sample-adaptive band-offset sign.
/// </summary>
/// <returns><see langword="true"/> for a negative offset; otherwise, <see langword="false"/>.</returns>
public bool ReadSampleAdaptiveOffsetSign() => this.decoder.ReadBypass();
/// <summary>
/// Decodes a horizontal last-significant-coefficient prefix flag.
/// </summary>
/// <param name="isChroma">A value indicating whether the coefficient belongs to a chroma channel.</param>
/// <param name="contextIndex">The block-size and prefix-derived context index within the channel.</param>
/// <returns>The decoded flag value.</returns>
public bool ReadLastSignificantX(bool isChroma, int contextIndex)
{
Span<HevcCabacContext> selectedContexts = this.contexts.LastSignificantX;
return this.decoder.ReadDecision(ref selectedContexts[(isChroma ? 15 : 0) + contextIndex]);
}
/// <summary>
/// Decodes a vertical last-significant-coefficient prefix flag.
/// </summary>
/// <param name="isChroma">A value indicating whether the coefficient belongs to a chroma channel.</param>
/// <param name="contextIndex">The block-size and prefix-derived context index within the channel.</param>
/// <returns>The decoded flag value.</returns>
public bool ReadLastSignificantY(bool isChroma, int contextIndex)
{
Span<HevcCabacContext> selectedContexts = this.contexts.LastSignificantY;
return this.decoder.ReadDecision(ref selectedContexts[(isChroma ? 15 : 0) + contextIndex]);
}
/// <summary>
/// Decodes a significant-coefficient-group flag.
/// </summary>
/// <param name="isChroma">A value indicating whether the coefficient group belongs to a chroma channel.</param>
/// <param name="contextIndex">The neighboring-group-derived context index.</param>
/// <returns>The decoded flag value.</returns>
public bool ReadSignificantCoefficientGroup(bool isChroma, int contextIndex)
{
Span<HevcCabacContext> selectedContexts = this.contexts.SignificantCoefficientGroup;
return this.decoder.ReadDecision(ref selectedContexts[(isChroma ? 2 : 0) + contextIndex]);
}
/// <summary>
/// Decodes a significant-coefficient flag.
/// </summary>
/// <param name="isChroma">A value indicating whether the coefficient belongs to a chroma channel.</param>
/// <param name="contextIndex">The scan-position-derived context index within the channel.</param>
/// <returns>The decoded flag value.</returns>
public bool ReadSignificantCoefficient(bool isChroma, int contextIndex)
{
Span<HevcCabacContext> selectedContexts = this.contexts.SignificantCoefficient;
return this.decoder.ReadDecision(ref selectedContexts[(isChroma ? 28 : 0) + contextIndex]);
}
/// <summary>
/// Decodes whether a significant coefficient has an absolute level greater than one.
/// </summary>
/// <param name="isChroma">A value indicating whether the coefficient belongs to a chroma channel.</param>
/// <param name="contextIndex">The coefficient-group and preceding-level-derived context index.</param>
/// <returns>The decoded flag value.</returns>
public bool ReadCoefficientGreaterThanOne(bool isChroma, int contextIndex)
{
Span<HevcCabacContext> selectedContexts = this.contexts.GreaterThanOne;
return this.decoder.ReadDecision(ref selectedContexts[(isChroma ? 16 : 0) + contextIndex]);
}
/// <summary>
/// Decodes whether the first eligible coefficient has an absolute level greater than two.
/// </summary>
/// <param name="isChroma">A value indicating whether the coefficient belongs to a chroma channel.</param>
/// <param name="contextIndex">The coefficient-group-derived context index within the channel.</param>
/// <returns>The decoded flag value.</returns>
public bool ReadCoefficientGreaterThanTwo(bool isChroma, int contextIndex)
{
Span<HevcCabacContext> selectedContexts = this.contexts.GreaterThanTwo;
return this.decoder.ReadDecision(ref selectedContexts[(isChroma ? 4 : 0) + contextIndex]);
}
/// <summary>
/// Decodes an absolute coefficient-level remainder.
/// </summary>
/// <param name="riceParameter">The current Golomb-Rice parameter.</param>
/// <param name="useLimitedPrefixLength">
/// A value indicating whether extended-precision processing limits the prefix length.
/// </param>
/// <param name="maximumLog2TransformDynamicRange">The channel's maximum transform dynamic range.</param>
/// <returns>The decoded nonnegative coefficient-level remainder.</returns>
/// <exception cref="InvalidImageContentException">The coded remainder exceeds a 32-bit unsigned value.</exception>
public uint ReadCoefficientRemaining(
int riceParameter,
bool useLimitedPrefixLength,
int maximumLog2TransformDynamicRange)
{
int longestPrefix = useLimitedPrefixLength
? 32 - maximumLog2TransformDynamicRange
: int.MaxValue;
// Extended-precision streams cap the unary prefix at the transform dynamic range. Reaching that cap
// implies the end of the prefix even when the final bypass bin is one, so no terminating zero is required.
int prefix = 0;
while (prefix < longestPrefix && this.decoder.ReadBypass())
{
prefix++;
}
if (prefix < CoefficientRemainingReduction)
{
uint suffix = this.decoder.ReadBypassBits(riceParameter);
ulong value = ((ulong)prefix << riceParameter) + suffix;
if (value > uint.MaxValue)
{
throw new InvalidImageContentException("The HEVC coefficient level is too large.");
}
return (uint)value;
}
int prefixLength = prefix - CoefficientRemainingReduction;
int suffixLength;
if (useLimitedPrefixLength)
{
int maximumPrefixLength = 32
- (CoefficientRemainingReduction + maximumLog2TransformDynamicRange);
suffixLength = prefixLength == maximumPrefixLength
? maximumLog2TransformDynamicRange - riceParameter
: prefixLength;
}
else
{
suffixLength = prefixLength;
}
int codedSuffixLength = suffixLength + riceParameter;
if (prefixLength >= 32 || codedSuffixLength > 32)
{
throw new InvalidImageContentException("The HEVC coefficient level is too large.");
}
// Prefixes beyond the first three represent an exponential-Golomb basis; the Rice parameter scales both
// that basis and the suffix while the bounded arithmetic reader supplies the remaining low bits.
uint codeWord = this.decoder.ReadBypassBits(codedSuffixLength);
ulong baseValue = (((1UL << prefixLength) - 1) + CoefficientRemainingReduction) << riceParameter;
ulong result = baseValue + codeWord;
if (result > uint.MaxValue)
{
throw new InvalidImageContentException("The HEVC coefficient level is too large.");
}
return (uint)result;
}
/// <summary>
/// Decodes a most-significant-bit-first sequence of equal-probability flags.
/// </summary>
/// <param name="bitCount">The number of flags to decode.</param>
/// <returns>The decoded unsigned value.</returns>
public uint ReadBypassBits(int bitCount) => this.decoder.ReadBypassBits(bitCount);
/// <summary>
/// Selects the byte-aligned range used by aligned bypass syntax.
/// </summary>
public void AlignBypass() => this.decoder.AlignBypass();
/// <summary>
/// Decodes the flag that terminates a coding-tree block or entropy substream.
/// </summary>
/// <returns>The decoded termination flag.</returns>
public bool ReadTerminate() => this.decoder.ReadTerminate();
/// <summary>
/// Validates the stop bit and zero padding after a terminating entropy-coded value.
/// </summary>
/// <exception cref="InvalidImageContentException">The entropy substream has invalid termination alignment.</exception>
public readonly void ValidateTerminationAlignment() => this.decoder.ValidateTerminationAlignment();
/// <summary>
/// Decodes a context-adaptive truncated-unary value.
/// </summary>
/// <param name="selectedContexts">The context set selected for the syntax element.</param>
/// <param name="firstContextIndex">The context used by the first binary decision.</param>
/// <param name="continuationContextIndex">The context used by each subsequent decision.</param>
/// <param name="maximumValue">The inclusive maximum decoded value.</param>
/// <returns>The decoded truncated-unary value.</returns>
private uint ReadTruncatedUnary(
Span<HevcCabacContext> selectedContexts,
int firstContextIndex,
int continuationContextIndex,
int maximumValue)
{
if (maximumValue == 0
|| !this.decoder.ReadDecision(ref selectedContexts[firstContextIndex]))
{
return 0;
}
uint value = 1;
while (value < maximumValue
&& this.decoder.ReadDecision(ref selectedContexts[continuationContextIndex]))
{
value++;
}
return value;
}
/// <summary>
/// Decodes an equal-probability exponential-Golomb value.
/// </summary>
/// <param name="order">The initial suffix width.</param>
/// <returns>The decoded unsigned value.</returns>
/// <exception cref="InvalidImageContentException">The coded value exceeds a 32-bit unsigned value.</exception>
private uint ReadBypassExponentialGolomb(int order)
{
ulong value = 0;
int suffixWidth = order;
while (this.decoder.ReadBypass())
{
if (suffixWidth >= 32)
{
throw new InvalidImageContentException("The HEVC exponential-Golomb value is too large.");
}
value += 1UL << suffixWidth;
suffixWidth++;
}
// Each leading one adds the basis for the current order and widens the final suffix by one bit.
value += this.decoder.ReadBypassBits(suffixWidth);
if (value > uint.MaxValue)
{
throw new InvalidImageContentException("The HEVC exponential-Golomb value is too large.");
}
return (uint)value;
}
}

40
src/ImageSharp/Formats/Heif/Hevc/HevcChromaSampleLocation.cs

@ -1,40 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Identifies the location of a 4:2:0 chroma sample relative to its associated two-by-two luma sample region.
/// </summary>
internal enum HevcChromaSampleLocation : byte
{
/// <summary>
/// The chroma sample is horizontally co-sited with the left luma column and vertically centered.
/// </summary>
Left = 0,
/// <summary>
/// The chroma sample is horizontally and vertically centered.
/// </summary>
Center = 1,
/// <summary>
/// The chroma sample is co-sited with the top-left luma sample.
/// </summary>
TopLeft = 2,
/// <summary>
/// The chroma sample is horizontally centered and co-sited with the top luma row.
/// </summary>
Top = 3,
/// <summary>
/// The chroma sample is horizontally co-sited with the left luma column and vertically co-sited with the bottom luma row.
/// </summary>
BottomLeft = 4,
/// <summary>
/// The chroma sample is horizontally centered and vertically co-sited with the bottom luma row.
/// </summary>
Bottom = 5,
}

381
src/ImageSharp/Formats/Heif/Hevc/HevcCodecConfiguration.cs

@ -1,381 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Contains the image-description fields and parameter-set arrays stored in an HEVC codec-configuration item
/// property.
/// </summary>
internal sealed class HevcCodecConfiguration
{
/// <summary>
/// The NAL-unit arrays carried by the codec-configuration property.
/// </summary>
private readonly HevcNalUnitArray[] nalUnitArrays;
/// <summary>
/// Initializes a new instance of the <see cref="HevcCodecConfiguration"/> class from an HEVC
/// codec-configuration item-property payload.
/// </summary>
/// <param name="data">The complete bounded configuration payload.</param>
public HevcCodecConfiguration(ReadOnlySpan<byte> data)
{
const int fixedRecordLength = 23;
if (data.Length < fixedRecordLength)
{
throw new InvalidImageContentException("The HEVC codec configuration is truncated.");
}
int offset = 0;
if (data[offset++] != 1)
{
throw new InvalidImageContentException("The HEVC codec configuration has an unsupported version.");
}
byte profile = data[offset++];
this.GeneralProfileSpace = (byte)(profile >> 6);
this.GeneralTierFlag = (profile & 0x20) != 0;
this.GeneralProfileIdc = (byte)(profile & 0x1F);
this.GeneralProfileCompatibilityFlags = BinaryPrimitives.ReadUInt32BigEndian(data[offset..]);
offset += 4;
this.GeneralConstraintIndicatorFlags = ((ulong)BinaryPrimitives.ReadUInt32BigEndian(data[offset..]) << 16)
| BinaryPrimitives.ReadUInt16BigEndian(data[(offset + 4)..]);
offset += 6;
this.GeneralLevelIdc = data[offset++];
ushort spatialSegmentation = BinaryPrimitives.ReadUInt16BigEndian(data[offset..]);
offset += 2;
byte parallelism = data[offset++];
byte chromaFormat = data[offset++];
byte lumaBitDepth = data[offset++];
byte chromaBitDepth = data[offset++];
if ((spatialSegmentation & 0xF000) != 0xF000
|| (parallelism & 0xFC) != 0xFC
|| (chromaFormat & 0xFC) != 0xFC
|| (lumaBitDepth & 0xF8) != 0xF8
|| (chromaBitDepth & 0xF8) != 0xF8)
{
throw new InvalidImageContentException("The HEVC codec configuration has invalid reserved bits.");
}
this.ChromaFormat = (byte)(chromaFormat & 3);
this.BitDepthLuma = 8 + (lumaBitDepth & 7);
this.BitDepthChroma = 8 + (chromaBitDepth & 7);
// Reject precisions outside the public HEIF profile matrix before an unrepresentable value can enter the
// typed image metadata or reach a sample pipeline that only implements 8, 10, and 12-bit arithmetic.
if (this.BitDepthLuma is not 8 and not 10 and not 12
|| (this.ChromaFormat != 0 && this.BitDepthChroma is not 8 and not 10 and not 12))
{
throw new InvalidImageContentException("The HEVC codec configuration uses an unsupported component bit depth.");
}
// Average frame rate and temporal-layer signaling describe timed samples. Consume those fixed-record fields
// to reach the image item's NAL length width without retaining playback state in the still-image model.
offset += 2;
byte temporalAndLengthFields = data[offset++];
int temporalLayerCount = (temporalAndLengthFields >> 3) & 7;
bool temporalIdNested = (temporalAndLengthFields & 4) != 0;
this.NalUnitLengthSize = (temporalAndLengthFields & 3) + 1;
int arrayCount = data[offset++];
this.nalUnitArrays = new HevcNalUnitArray[arrayCount];
Span<bool> seenNalUnitTypes = stackalloc bool[64];
for (int arrayIndex = 0; arrayIndex < arrayCount; arrayIndex++)
{
if (data.Length - offset < 3)
{
throw new InvalidImageContentException("The HEVC codec configuration contains a truncated NAL-unit array header.");
}
byte arrayHeader = data[offset++];
if ((arrayHeader & 0x40) != 0)
{
throw new InvalidImageContentException("The HEVC codec configuration NAL-unit array has a nonzero reserved bit.");
}
bool isComplete = (arrayHeader & 0x80) != 0;
byte nalUnitType = (byte)(arrayHeader & 0x3F);
if (seenNalUnitTypes[nalUnitType])
{
throw new InvalidImageContentException($"The HEVC codec configuration contains more than one array for NAL-unit type {nalUnitType}.");
}
seenNalUnitTypes[nalUnitType] = true;
int nalUnitCount = BinaryPrimitives.ReadUInt16BigEndian(data[offset..]);
offset += 2;
HevcNalUnit[] nalUnits = new HevcNalUnit[nalUnitCount];
for (int nalUnitIndex = 0; nalUnitIndex < nalUnitCount; nalUnitIndex++)
{
if (data.Length - offset < 2)
{
throw new InvalidImageContentException("The HEVC codec configuration contains a truncated NAL-unit length.");
}
int nalUnitLength = BinaryPrimitives.ReadUInt16BigEndian(data[offset..]);
offset += 2;
if (nalUnitLength < 2 || nalUnitLength > data.Length - offset)
{
throw new InvalidImageContentException("The HEVC codec configuration contains an invalid NAL-unit length.");
}
HevcNalUnit nalUnit = new(data.Slice(offset, nalUnitLength));
// The array header repeats the type so a damaged or misrouted parameter set is rejected before
// its RBSP syntax can affect the image configuration.
if (nalUnit.Header.NalUnitType != nalUnitType)
{
throw new InvalidImageContentException("The HEVC codec configuration NAL-unit type does not match its array.");
}
nalUnits[nalUnitIndex] = nalUnit;
offset += nalUnitLength;
}
this.nalUnitArrays[arrayIndex] = new HevcNalUnitArray(nalUnitType, isComplete, nalUnits);
}
if (offset != data.Length)
{
throw new InvalidImageContentException("The HEVC codec configuration contains unexpected trailing data.");
}
List<HevcVideoParameterSet> videoParameterSets = new();
foreach (HevcNalUnitArray nalUnitArray in this.nalUnitArrays)
{
const byte videoParameterSetNalUnitType = 32;
if (nalUnitArray.NalUnitType != videoParameterSetNalUnitType)
{
continue;
}
foreach (HevcNalUnit nalUnit in nalUnitArray.NalUnits)
{
HevcVideoParameterSet videoParameterSet = new(nalUnit);
// Legacy HEIC muxers commonly preserve only the original four source/packing constraint bits in
// hvcC and zero later profile-specific constraint bits. SPS validation provides the authoritative
// chroma and bit-depth checks, so do not reject otherwise matching Range Extensions images here.
if (!videoParameterSet.ProfileTierLevel.Matches(this)
|| (temporalLayerCount != 0 && videoParameterSet.MaxSubLayers != temporalLayerCount)
|| (temporalLayerCount != 0 && videoParameterSet.TemporalIdNestingFlag != temporalIdNested))
{
throw new InvalidImageContentException("The HEVC video parameter set does not match its codec configuration.");
}
videoParameterSets.Add(videoParameterSet);
}
}
this.VideoParameterSets = videoParameterSets;
List<HevcSequenceParameterSet> sequenceParameterSets = new();
foreach (HevcNalUnitArray nalUnitArray in this.nalUnitArrays)
{
const byte sequenceParameterSetNalUnitType = 33;
if (nalUnitArray.NalUnitType != sequenceParameterSetNalUnitType)
{
continue;
}
foreach (HevcNalUnit nalUnit in nalUnitArray.NalUnits)
{
HevcSequenceParameterSet sequenceParameterSet = new(nalUnit);
bool referencesKnownVideoParameterSet = false;
foreach (HevcVideoParameterSet videoParameterSet in videoParameterSets)
{
referencesKnownVideoParameterSet |= videoParameterSet.Id == sequenceParameterSet.VideoParameterSetId;
}
if (!referencesKnownVideoParameterSet
|| !sequenceParameterSet.ProfileTierLevel.Matches(this)
|| sequenceParameterSet.ChromaFormat != this.ChromaFormat
|| sequenceParameterSet.BitDepthLuma != this.BitDepthLuma
|| sequenceParameterSet.BitDepthChroma != this.BitDepthChroma
|| (temporalLayerCount != 0 && sequenceParameterSet.MaxSubLayers != temporalLayerCount)
|| (temporalLayerCount != 0 && sequenceParameterSet.TemporalIdNestingFlag != temporalIdNested))
{
throw new InvalidImageContentException("The HEVC sequence parameter set does not match its codec configuration.");
}
sequenceParameterSets.Add(sequenceParameterSet);
}
}
this.SequenceParameterSets = sequenceParameterSets;
List<HevcPictureParameterSet> pictureParameterSets = new();
foreach (HevcNalUnitArray nalUnitArray in this.nalUnitArrays)
{
const byte pictureParameterSetNalUnitType = 34;
if (nalUnitArray.NalUnitType != pictureParameterSetNalUnitType)
{
continue;
}
foreach (HevcNalUnit nalUnit in nalUnitArray.NalUnits)
{
pictureParameterSets.Add(new HevcPictureParameterSet(nalUnit, sequenceParameterSets));
}
}
this.PictureParameterSets = pictureParameterSets;
}
/// <summary>
/// Gets the profile namespace declared by the coded image.
/// </summary>
public byte GeneralProfileSpace { get; }
/// <summary>
/// Gets a value indicating whether the coded image uses the high tier.
/// </summary>
public bool GeneralTierFlag { get; }
/// <summary>
/// Gets the profile identifier declared by the coded image.
/// </summary>
public byte GeneralProfileIdc { get; }
/// <summary>
/// Gets the profile-compatibility flags declared by the coded image.
/// </summary>
public uint GeneralProfileCompatibilityFlags { get; }
/// <summary>
/// Gets the 48-bit profile-constraint flags declared by the coded image.
/// </summary>
public ulong GeneralConstraintIndicatorFlags { get; }
/// <summary>
/// Gets the level identifier declared by the coded image.
/// </summary>
public byte GeneralLevelIdc { get; }
/// <summary>
/// Gets the coded chroma format, where zero denotes monochrome and one through three denote 4:2:0, 4:2:2,
/// and 4:4:4 respectively.
/// </summary>
public byte ChromaFormat { get; }
/// <summary>
/// Gets the coded luma sample precision in bits.
/// </summary>
public int BitDepthLuma { get; }
/// <summary>
/// Gets the coded chroma sample precision in bits.
/// </summary>
public int BitDepthChroma { get; }
/// <summary>
/// Gets the maximum coded color-component precision in bits.
/// </summary>
public HeifBitDepth BitDepth
=> (HeifBitDepth)(this.IsMonochrome ? this.BitDepthLuma : Math.Max(this.BitDepthLuma, this.BitDepthChroma));
/// <summary>
/// Gets a value indicating whether the coded image contains only a luma plane.
/// </summary>
public bool IsMonochrome => this.ChromaFormat == 0;
/// <summary>
/// Gets the number of bytes used by each length-delimited NAL unit in the associated image item.
/// </summary>
public int NalUnitLengthSize { get; }
/// <summary>
/// Gets the bounded NAL-unit arrays carried by the codec-configuration property.
/// </summary>
public IReadOnlyList<HevcNalUnitArray> NalUnitArrays => this.nalUnitArrays;
/// <summary>
/// Gets the validated video parameter sets carried by the codec-configuration property.
/// </summary>
public IReadOnlyList<HevcVideoParameterSet> VideoParameterSets { get; }
/// <summary>
/// Gets the validated sequence parameter sets carried by the codec-configuration property.
/// </summary>
public IReadOnlyList<HevcSequenceParameterSet> SequenceParameterSets { get; }
/// <summary>
/// Gets the validated picture parameter sets carried by the codec-configuration property.
/// </summary>
public IReadOnlyList<HevcPictureParameterSet> PictureParameterSets { get; }
/// <summary>
/// Validates the associated pixel-information property against the coded luma and chroma sample precisions.
/// </summary>
/// <param name="channelBitDepths">The per-channel precisions associated with the HEVC image item.</param>
public void ValidateChannelBitDepths(ReadOnlySpan<byte> channelBitDepths)
{
int expectedChannelCount = this.IsMonochrome ? 1 : 3;
if (channelBitDepths.Length != expectedChannelCount || channelBitDepths[0] != this.BitDepthLuma)
{
throw new InvalidImageContentException("The HEVC item pixel information does not match its codec configuration.");
}
for (int channel = 1; channel < channelBitDepths.Length; channel++)
{
if (channelBitDepths[channel] != this.BitDepthChroma)
{
throw new InvalidImageContentException("The HEVC item pixel information does not match its codec configuration.");
}
}
}
/// <summary>
/// Determines whether another configuration describes the same coded-image sample layout.
/// </summary>
/// <param name="other">The configuration to compare.</param>
/// <returns><see langword="true"/> when the profile, level, chroma format, and sample precisions match.</returns>
public bool HasMatchingImageConfiguration(HevcCodecConfiguration other)
=> this.GeneralProfileSpace == other.GeneralProfileSpace
&& this.GeneralTierFlag == other.GeneralTierFlag
&& this.GeneralProfileIdc == other.GeneralProfileIdc
&& this.GeneralProfileCompatibilityFlags == other.GeneralProfileCompatibilityFlags
&& this.GeneralConstraintIndicatorFlags == other.GeneralConstraintIndicatorFlags
&& this.GeneralLevelIdc == other.GeneralLevelIdc
&& this.ChromaFormat == other.ChromaFormat
&& this.BitDepthLuma == other.BitDepthLuma
&& this.BitDepthChroma == other.BitDepthChroma;
}
/// <summary>
/// Contains every configuration NAL unit declared for one HEVC NAL-unit type.
/// </summary>
internal sealed class HevcNalUnitArray
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcNalUnitArray"/> class.
/// </summary>
/// <param name="nalUnitType">The six-bit HEVC NAL-unit type.</param>
/// <param name="isComplete">A value indicating whether the array contains every NAL unit of this type.</param>
/// <param name="nalUnits">The decoded bounded NAL units.</param>
public HevcNalUnitArray(byte nalUnitType, bool isComplete, HevcNalUnit[] nalUnits)
{
this.NalUnitType = nalUnitType;
this.IsComplete = isComplete;
this.NalUnits = nalUnits;
}
/// <summary>
/// Gets the six-bit HEVC NAL-unit type shared by every entry in the array.
/// </summary>
public byte NalUnitType { get; }
/// <summary>
/// Gets a value indicating whether the array contains every NAL unit of this type for the coded image.
/// </summary>
public bool IsComplete { get; }
/// <summary>
/// Gets the decoded NAL units.
/// </summary>
public IReadOnlyList<HevcNalUnit> NalUnits { get; }
}

53
src/ImageSharp/Formats/Heif/Hevc/HevcCodedBlockFlags.cs

@ -1,53 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Contains one or two coded-block flags for a square or vertically split HEVC component transform section.
/// </summary>
internal readonly struct HevcCodedBlockFlags
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcCodedBlockFlags"/> struct for one square block.
/// </summary>
/// <param name="first">The square block's coded-block flag.</param>
public HevcCodedBlockFlags(bool first)
{
this.First = first;
this.Second = false;
this.IsSplit = false;
}
/// <summary>
/// Initializes a new instance of the <see cref="HevcCodedBlockFlags"/> struct for two rectangular sub-blocks.
/// </summary>
/// <param name="first">The first square sub-block's coded-block flag.</param>
/// <param name="second">The second square sub-block's coded-block flag.</param>
public HevcCodedBlockFlags(bool first, bool second)
{
this.First = first;
this.Second = second;
this.IsSplit = true;
}
/// <summary>
/// Gets a value indicating whether the first or only coefficient block contains coded residual data.
/// </summary>
public bool First { get; }
/// <summary>
/// Gets a value indicating whether the second rectangular sub-block contains coded residual data.
/// </summary>
public bool Second { get; }
/// <summary>
/// Gets a value indicating whether two square sub-block flags are present.
/// </summary>
public bool IsSplit { get; }
/// <summary>
/// Gets a value indicating whether either governed coefficient block contains coded residual data.
/// </summary>
public bool Any => this.First || this.Second;
}

217
src/ImageSharp/Formats/Heif/Hevc/HevcCodingTreeState.cs

@ -1,217 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Stores the spatial coding-unit state required by later HEVC still-picture syntax and reconstruction stages.
/// </summary>
internal sealed class HevcCodingTreeState : IDisposable
{
/// <summary>
/// The coding-unit flag indicating transform and quantization bypass.
/// </summary>
private const byte TransquantBypassFlag = 1 << 0;
/// <summary>
/// The coding-unit flag indicating pulse-code-modulated samples.
/// </summary>
private const byte PcmFlag = 1 << 1;
/// <summary>
/// The decoded coding-unit depth at minimum-coding-block resolution.
/// </summary>
private readonly Buffer2D<byte> depths;
/// <summary>
/// The effective luma quantization parameter at minimum-coding-block resolution.
/// </summary>
private readonly Buffer2D<sbyte> quantizationParameters;
/// <summary>
/// The packed bypass and PCM flags at minimum-coding-block resolution.
/// </summary>
private readonly Buffer2D<byte> flags;
/// <summary>
/// Initializes a new instance of the <see cref="HevcCodingTreeState"/> class.
/// </summary>
/// <param name="configuration">The configuration providing the image memory allocator.</param>
/// <param name="sequenceParameterSet">The coded picture and minimum coding-block geometry.</param>
public HevcCodingTreeState(Configuration configuration, HevcSequenceParameterSet sequenceParameterSet)
{
this.MinCodingBlockLog2 = sequenceParameterSet.MinCodingBlockLog2;
this.WidthInMinCodingBlocks = DivideCeilingByPowerOfTwo(
sequenceParameterSet.Width,
this.MinCodingBlockLog2);
this.HeightInMinCodingBlocks = DivideCeilingByPowerOfTwo(
sequenceParameterSet.Height,
this.MinCodingBlockLog2);
Buffer2D<byte>? depths = null;
Buffer2D<sbyte>? quantizationParameters = null;
Buffer2D<byte>? flags = null;
try
{
depths = configuration.MemoryAllocator.Allocate2D<byte>(
this.WidthInMinCodingBlocks,
this.HeightInMinCodingBlocks);
quantizationParameters = configuration.MemoryAllocator.Allocate2D<sbyte>(
this.WidthInMinCodingBlocks,
this.HeightInMinCodingBlocks);
flags = configuration.MemoryAllocator.Allocate2D<byte>(
this.WidthInMinCodingBlocks,
this.HeightInMinCodingBlocks);
this.depths = depths;
this.quantizationParameters = quantizationParameters;
this.flags = flags;
}
catch
{
flags?.Dispose();
quantizationParameters?.Dispose();
depths?.Dispose();
throw;
}
}
/// <summary>
/// Gets the base-two logarithm of the state map's luma sample unit.
/// </summary>
public int MinCodingBlockLog2 { get; }
/// <summary>
/// Gets the state-map width in minimum coding blocks.
/// </summary>
public int WidthInMinCodingBlocks { get; }
/// <summary>
/// Gets the state-map height in minimum coding blocks.
/// </summary>
public int HeightInMinCodingBlocks { get; }
/// <summary>
/// Gets the split-flag context derived from available left and above coding units.
/// </summary>
/// <param name="x">The current coding-unit left coordinate in luma samples.</param>
/// <param name="y">The current coding-unit top coordinate in luma samples.</param>
/// <param name="depth">The current coding-tree depth.</param>
/// <param name="leftAvailable">A value indicating whether the left coding unit is available for prediction.</param>
/// <param name="aboveAvailable">A value indicating whether the above coding unit is available for prediction.</param>
/// <returns>The split context in the inclusive range zero through two.</returns>
public int GetSplitContext(int x, int y, int depth, bool leftAvailable, bool aboveAvailable)
{
int unitX = x >> this.MinCodingBlockLog2;
int unitY = y >> this.MinCodingBlockLog2;
int context = 0;
if (leftAvailable && this.depths.DangerousGetRowSpan(unitY)[unitX - 1] > depth)
{
context++;
}
if (aboveAvailable && this.depths.DangerousGetRowSpan(unitY - 1)[unitX] > depth)
{
context++;
}
return context;
}
/// <summary>
/// Records the state shared by every minimum coding block covered by one leaf coding unit.
/// </summary>
/// <param name="x">The coding-unit left coordinate in luma samples.</param>
/// <param name="y">The coding-unit top coordinate in luma samples.</param>
/// <param name="log2Size">The base-two logarithm of the square coding-unit size.</param>
/// <param name="depth">The coding-tree depth.</param>
/// <param name="quantizationParameter">The effective luma quantization parameter.</param>
/// <param name="transquantBypass">A value indicating whether transform and quantization are bypassed.</param>
/// <param name="pcm">A value indicating whether the coding unit contains pulse-code-modulated samples.</param>
public void SetCodingUnit(
int x,
int y,
int log2Size,
int depth,
int quantizationParameter,
bool transquantBypass,
bool pcm)
{
int unitX = x >> this.MinCodingBlockLog2;
int unitY = y >> this.MinCodingBlockLog2;
int unitCount = 1 << (log2Size - this.MinCodingBlockLog2);
int endX = Math.Min(unitX + unitCount, this.WidthInMinCodingBlocks);
int endY = Math.Min(unitY + unitCount, this.HeightInMinCodingBlocks);
byte packedFlags = (byte)((transquantBypass ? TransquantBypassFlag : 0) | (pcm ? PcmFlag : 0));
// Edge coding units still cover a complete power-of-two block in syntax, but the state map contains only
// displayed picture coordinates. Clipping here keeps later neighbor lookup within the owned picture state.
for (int row = unitY; row < endY; row++)
{
this.depths.DangerousGetRowSpan(row)[unitX..endX].Fill((byte)depth);
this.quantizationParameters.DangerousGetRowSpan(row)[unitX..endX].Fill((sbyte)quantizationParameter);
this.flags.DangerousGetRowSpan(row)[unitX..endX].Fill(packedFlags);
}
}
/// <summary>
/// Gets the recorded coding-tree depth at a luma sample coordinate.
/// </summary>
/// <param name="x">The luma sample X coordinate.</param>
/// <param name="y">The luma sample Y coordinate.</param>
/// <returns>The leaf coding-unit depth.</returns>
public int GetDepth(int x, int y)
=> this.depths.DangerousGetRowSpan(y >> this.MinCodingBlockLog2)[x >> this.MinCodingBlockLog2];
/// <summary>
/// Gets the effective luma quantization parameter at a luma sample coordinate.
/// </summary>
/// <param name="x">The luma sample X coordinate.</param>
/// <param name="y">The luma sample Y coordinate.</param>
/// <returns>The effective luma quantization parameter.</returns>
public int GetQuantizationParameter(int x, int y)
=> this.quantizationParameters.DangerousGetRowSpan(y >> this.MinCodingBlockLog2)[x >> this.MinCodingBlockLog2];
/// <summary>
/// Gets a value indicating whether the coding unit at a luma sample coordinate bypasses transform and quantization.
/// </summary>
/// <param name="x">The luma sample X coordinate.</param>
/// <param name="y">The luma sample Y coordinate.</param>
/// <returns><see langword="true"/> when bypass is enabled; otherwise, <see langword="false"/>.</returns>
public bool IsTransquantBypass(int x, int y)
=> (this.flags.DangerousGetRowSpan(y >> this.MinCodingBlockLog2)[x >> this.MinCodingBlockLog2]
& TransquantBypassFlag) != 0;
/// <summary>
/// Gets a value indicating whether the coding unit at a luma sample coordinate contains PCM samples.
/// </summary>
/// <param name="x">The luma sample X coordinate.</param>
/// <param name="y">The luma sample Y coordinate.</param>
/// <returns><see langword="true"/> for pulse-code-modulated samples; otherwise, <see langword="false"/>.</returns>
public bool IsPcm(int x, int y)
=> (this.flags.DangerousGetRowSpan(y >> this.MinCodingBlockLog2)[x >> this.MinCodingBlockLog2]
& PcmFlag) != 0;
/// <summary>
/// Releases the owned coding-tree state maps.
/// </summary>
public void Dispose()
{
this.depths.Dispose();
this.quantizationParameters.Dispose();
this.flags.Dispose();
}
/// <summary>
/// Divides a nonnegative sample count by a power of two with upward rounding.
/// </summary>
/// <param name="value">The sample count.</param>
/// <param name="shift">The base-two divisor logarithm.</param>
/// <returns>The upward-rounded quotient.</returns>
private static int DivideCeilingByPowerOfTwo(int value, int shift) => (value + (1 << shift) - 1) >> shift;
}

346
src/ImageSharp/Formats/Heif/Hevc/HevcCoefficientCodingParameters.cs

@ -1,346 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Contains the immutable entropy-coding parameters for one HEVC transform block.
/// </summary>
internal readonly struct HevcCoefficientCodingParameters
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcCoefficientCodingParameters"/> struct.
/// </summary>
/// <param name="width">The transform-block width.</param>
/// <param name="height">The transform-block height.</param>
/// <param name="plane">The reconstructed component.</param>
/// <param name="scanType">The coefficient scan selected for the block.</param>
/// <param name="useSingleSignificanceContext">Whether transform skip or transquant bypass selects the single significance context.</param>
/// <param name="signDataHidingEnabled">Whether the first coefficient sign in an eligible group is inferred.</param>
/// <param name="persistentRiceAdaptationEnabled">Whether Rice parameters adapt across transform blocks.</param>
/// <param name="cabacBypassAlignmentEnabled">Whether coefficient bypass data is byte aligned when escape data is present.</param>
/// <param name="extendedPrecisionProcessingEnabled">Whether coefficient remainders use the bounded extended-precision prefix.</param>
/// <param name="maximumLog2TransformDynamicRange">The component transform dynamic range excluding its sign bit.</param>
/// <param name="riceStatisticsIndex">The luma/chroma and transformed/non-transformed Rice statistics selector.</param>
public HevcCoefficientCodingParameters(
int width,
int height,
HevcPlane plane,
HevcCoefficientScanType scanType,
bool useSingleSignificanceContext,
bool signDataHidingEnabled,
bool persistentRiceAdaptationEnabled,
bool cabacBypassAlignmentEnabled,
bool extendedPrecisionProcessingEnabled,
int maximumLog2TransformDynamicRange,
int riceStatisticsIndex)
{
this.Width = width;
this.Height = height;
this.Plane = plane;
this.ScanType = scanType;
this.FirstSignificanceMapContext = GetFirstSignificanceMapContext(width, height, plane != HevcPlane.Y, scanType, useSingleSignificanceContext);
this.SignDataHidingEnabled = signDataHidingEnabled;
this.PersistentRiceAdaptationEnabled = persistentRiceAdaptationEnabled;
this.CabacBypassAlignmentEnabled = cabacBypassAlignmentEnabled;
this.ExtendedPrecisionProcessingEnabled = extendedPrecisionProcessingEnabled;
this.MaximumLog2TransformDynamicRange = maximumLog2TransformDynamicRange;
this.RiceStatisticsIndex = riceStatisticsIndex;
}
/// <summary>
/// Gets the transform-block width.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the transform-block height.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets the reconstructed component.
/// </summary>
public HevcPlane Plane { get; }
/// <summary>
/// Gets the coefficient scan selected for the block.
/// </summary>
public HevcCoefficientScanType ScanType { get; }
/// <summary>
/// Gets the first significant-coefficient context within the component context set.
/// </summary>
public int FirstSignificanceMapContext { get; }
/// <summary>
/// Gets a value indicating whether an eligible first coefficient sign is inferred from the group parity.
/// </summary>
public bool SignDataHidingEnabled { get; }
/// <summary>
/// Gets a value indicating whether Rice parameters adapt across transform blocks.
/// </summary>
public bool PersistentRiceAdaptationEnabled { get; }
/// <summary>
/// Gets a value indicating whether coefficient bypass data is byte aligned when escape data is present.
/// </summary>
public bool CabacBypassAlignmentEnabled { get; }
/// <summary>
/// Gets a value indicating whether coefficient remainders use the bounded extended-precision prefix.
/// </summary>
public bool ExtendedPrecisionProcessingEnabled { get; }
/// <summary>
/// Gets the component transform dynamic range excluding its sign bit.
/// </summary>
public int MaximumLog2TransformDynamicRange { get; }
/// <summary>
/// Gets the luma/chroma and transformed/non-transformed Rice statistics selector.
/// </summary>
public int RiceStatisticsIndex { get; }
/// <summary>
/// Gets the raster-position context mapping for a 4 by 4 transform block.
/// </summary>
private static ReadOnlySpan<byte> SignificanceContexts4x4 =>
[
0, 1, 4, 5,
2, 3, 4, 5,
6, 6, 8, 8,
7, 7, 8, 8,
];
/// <summary>
/// Creates the coefficient parameters selected by the active sequence, picture, and transform-unit state.
/// </summary>
/// <param name="pictureParameterSet">The active picture parameters.</param>
/// <param name="width">The transform-block width.</param>
/// <param name="height">The transform-block height.</param>
/// <param name="plane">The reconstructed component.</param>
/// <param name="isIntra">Whether the containing coding unit uses intra prediction.</param>
/// <param name="intraPredictionMode">The effective intra prediction mode, or a value ignored for inter prediction.</param>
/// <param name="transformSkip">Whether the transform block bypasses the inverse transform.</param>
/// <param name="transquantBypass">Whether the coding unit bypasses inverse quantization and inverse transform.</param>
/// <param name="residualDpcmMode">The residual differential-pulse-code-modulation mode selected for the block.</param>
/// <param name="useLumaSyntax">Whether a separately coded color plane uses the luma coefficient context set.</param>
/// <returns>The coefficient entropy-coding parameters for the transform block.</returns>
public static HevcCoefficientCodingParameters Create(
HevcPictureParameterSet pictureParameterSet,
int width,
int height,
HevcPlane plane,
bool isIntra,
int intraPredictionMode,
bool transformSkip,
bool transquantBypass,
HevcResidualDpcmMode residualDpcmMode,
bool useLumaSyntax = false)
{
HevcSequenceParameterSet sequenceParameterSet = pictureParameterSet.SequenceParameterSet;
HevcPlane codingPlane = useLumaSyntax ? HevcPlane.Y : plane;
bool isChroma = codingPlane != HevcPlane.Y;
bool nonTransformed = transformSkip || transquantBypass;
HevcCoefficientScanType scanType = SelectScanType(
width,
height,
codingPlane,
isIntra,
intraPredictionMode,
sequenceParameterSet.ChromaFormat,
sequenceParameterSet.SeparateColorPlaneFlag);
return new HevcCoefficientCodingParameters(
width,
height,
plane,
scanType,
sequenceParameterSet.TransformSkipContextEnabled && nonTransformed,
pictureParameterSet.SignDataHidingEnabled && !transquantBypass && residualDpcmMode == HevcResidualDpcmMode.None,
sequenceParameterSet.PersistentRiceAdaptationEnabled,
sequenceParameterSet.CabacBypassAlignmentEnabled,
sequenceParameterSet.ExtendedPrecisionProcessingEnabled,
sequenceParameterSet.GetMaxTransformDynamicRange(plane),
(isChroma ? 2 : 0) + (nonTransformed ? 1 : 0));
}
/// <summary>
/// Selects the scan direction from transform geometry and the effective intra prediction direction.
/// </summary>
/// <param name="width">The transform-block width.</param>
/// <param name="height">The transform-block height.</param>
/// <param name="plane">The reconstructed component.</param>
/// <param name="isIntra">Whether the containing coding unit uses intra prediction.</param>
/// <param name="intraPredictionMode">The effective intra prediction mode.</param>
/// <param name="chromaFormat">The sequence chroma-format identifier.</param>
/// <param name="separateColorPlane">Whether each 4:4:4 component is coded as an independent color plane.</param>
/// <returns>The selected coefficient scan.</returns>
public static HevcCoefficientScanType SelectScanType(
int width,
int height,
HevcPlane plane,
bool isIntra,
int intraPredictionMode,
byte chromaFormat,
bool separateColorPlane)
{
if (!isIntra)
{
return HevcCoefficientScanType.Diagonal;
}
bool isSubsampledChroma = plane != HevcPlane.Y && !separateColorPlane;
int subsamplingX = isSubsampledChroma && chromaFormat is 1 or 2 ? 1 : 0;
int subsamplingY = isSubsampledChroma && chromaFormat == 1 ? 1 : 0;
if (width > (8 >> subsamplingX) || height > (8 >> subsamplingY))
{
return HevcCoefficientScanType.Diagonal;
}
int mode = plane != HevcPlane.Y && chromaFormat == 2 && !separateColorPlane
? HevcIntraPredictionMode.RemapChroma422(intraPredictionMode)
: intraPredictionMode;
// Modes close to vertical place correlated residuals along rows, while modes close to horizontal use the
// transposed column scan. All other modes retain the diagonal scan.
if (Math.Abs(mode - HevcIntraPredictionMode.Vertical) <= 4)
{
return HevcCoefficientScanType.Horizontal;
}
return Math.Abs(mode - HevcIntraPredictionMode.Horizontal) <= 4
? HevcCoefficientScanType.Vertical
: HevcCoefficientScanType.Diagonal;
}
/// <summary>
/// Derives the coded-sub-block significance context from already decoded right and lower groups.
/// </summary>
/// <param name="groupFlags">The raster-ordered significant-group flags.</param>
/// <param name="groupX">The current group horizontal coordinate.</param>
/// <param name="groupY">The current group vertical coordinate.</param>
/// <returns>Zero when neither neighbor is significant; otherwise, one.</returns>
public int GetSignificantGroupContext(ReadOnlySpan<int> groupFlags, int groupX, int groupY)
{
int widthInGroups = this.Width / 4;
int heightInGroups = this.Height / 4;
bool rightSignificant = groupX < widthInGroups - 1 && groupFlags[(groupY * widthInGroups) + groupX + 1] != 0;
bool lowerSignificant = groupY < heightInGroups - 1 && groupFlags[((groupY + 1) * widthInGroups) + groupX] != 0;
return rightSignificant || lowerSignificant ? 1 : 0;
}
/// <summary>
/// Derives the two-bit right-and-lower significance pattern for coefficient contexts.
/// </summary>
/// <param name="groupFlags">The raster-ordered significant-group flags.</param>
/// <param name="groupX">The current group horizontal coordinate.</param>
/// <param name="groupY">The current group vertical coordinate.</param>
/// <returns>The right flag in bit zero and the lower flag in bit one.</returns>
public int GetSignificancePattern(ReadOnlySpan<int> groupFlags, int groupX, int groupY)
{
int widthInGroups = this.Width / 4;
int heightInGroups = this.Height / 4;
int right = groupX < widthInGroups - 1 && groupFlags[(groupY * widthInGroups) + groupX + 1] != 0 ? 1 : 0;
int lower = groupY < heightInGroups - 1 && groupFlags[((groupY + 1) * widthInGroups) + groupX] != 0 ? 1 : 0;
return right + (lower << 1);
}
/// <summary>
/// Derives the significant-coefficient context from its position and neighboring coefficient groups.
/// </summary>
/// <param name="rasterPosition">The coefficient raster position.</param>
/// <param name="significancePattern">The right-and-lower significant-group pattern.</param>
/// <returns>The context index within the component significance-map context set.</returns>
public int GetSignificantCoefficientContext(int rasterPosition, int significancePattern)
{
bool isChroma = this.Plane != HevcPlane.Y;
if (this.FirstSignificanceMapContext == (isChroma ? 15 : 27))
{
return this.FirstSignificanceMapContext;
}
int y = rasterPosition / this.Width;
int x = rasterPosition - (y * this.Width);
if (x + y == 0)
{
return 0;
}
if (this.Width == 4 && this.Height == 4)
{
return SignificanceContexts4x4[(y * 4) + x];
}
int context;
switch (significancePattern)
{
case 0:
int positionInGroup = (x & 3) + (y & 3);
context = positionInGroup >= 3 ? 0 : positionInGroup >= 1 ? 1 : 2;
break;
case 1:
int yInGroup = y & 3;
context = yInGroup >= 2 ? 0 : yInGroup >= 1 ? 1 : 2;
break;
case 2:
int xInGroup = x & 3;
context = xInGroup >= 2 ? 0 : xInGroup >= 1 ? 1 : 2;
break;
default:
context = 2;
break;
}
bool isBeyondFirstGroup = (x >> 2) + (y >> 2) > 0;
return this.FirstSignificanceMapContext + (isBeyondFirstGroup && !isChroma ? 3 : 0) + context;
}
/// <summary>
/// Selects the greater-than-one and greater-than-two context set for one coefficient group.
/// </summary>
/// <param name="subset">The coefficient-group scan index.</param>
/// <param name="foundGreaterThanOne">Whether the preceding group ended after finding a coefficient greater than one.</param>
/// <returns>The zero-based context set within the component context range.</returns>
public int GetLevelContextSet(int subset, bool foundGreaterThanOne)
{
int nonFirstSubsetOffset = this.Plane == HevcPlane.Y && subset > 0 ? 2 : 0;
return nonFirstSubsetOffset + (foundGreaterThanOne ? 1 : 0);
}
/// <summary>
/// Derives the first significant-coefficient context within one component context set.
/// </summary>
/// <param name="width">The transform-block width.</param>
/// <param name="height">The transform-block height.</param>
/// <param name="isChroma">Whether the transform block belongs to a chroma channel.</param>
/// <param name="scanType">The selected coefficient scan.</param>
/// <param name="useSingleSignificanceContext">Whether Range Extensions selects the single-context mode.</param>
/// <returns>The first significant-coefficient context index.</returns>
private static int GetFirstSignificanceMapContext(
int width,
int height,
bool isChroma,
HevcCoefficientScanType scanType,
bool useSingleSignificanceContext)
{
if (useSingleSignificanceContext)
{
return isChroma ? 15 : 27;
}
if (width == 4 && height == 4)
{
return 0;
}
if (width == 8 && height == 8)
{
return isChroma ? 9 : scanType == HevcCoefficientScanType.Diagonal ? 9 : 15;
}
return isChroma ? 12 : 21;
}
}

422
src/ImageSharp/Formats/Heif/Hevc/HevcCoefficientDecoder.cs

@ -1,422 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Numerics;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Decodes HEVC transform coefficients while retaining entropy-substream Rice state and reusable scratch storage.
/// </summary>
internal sealed class HevcCoefficientDecoder : IDisposable
{
/// <summary>
/// The maximum coefficient count in a 32 by 32 transform block.
/// </summary>
private const int MaximumCoefficientCount = 32 * 32;
/// <summary>
/// The maximum number of 4 by 4 coefficient groups in a transform block.
/// </summary>
private const int MaximumCoefficientGroupCount = MaximumCoefficientCount / 16;
/// <summary>
/// The maximum number of significant coefficients in one coefficient group.
/// </summary>
private const int CoefficientsPerGroup = 16;
/// <summary>
/// The maximum number of greater-than-one flags coded in one coefficient group.
/// </summary>
private const int GreaterThanOneFlagCount = 8;
/// <summary>
/// The minimum scan-position separation that enables sign-data hiding.
/// </summary>
private const int SignDataHidingThreshold = 4;
/// <summary>
/// The divisor that converts a persistent adaptation statistic to its Rice parameter.
/// </summary>
private const int RiceAdaptationDivisor = 4;
/// <summary>
/// The first scratch index occupied by coefficient-group significance flags.
/// </summary>
private const int CoefficientGroupFlagsOffset = MaximumCoefficientCount;
/// <summary>
/// The first scratch index occupied by significant coefficient raster positions.
/// </summary>
private const int CoefficientPositionsOffset = CoefficientGroupFlagsOffset + MaximumCoefficientGroupCount;
/// <summary>
/// The first scratch index occupied by absolute coefficient levels.
/// </summary>
private const int AbsoluteLevelsOffset = CoefficientPositionsOffset + CoefficientsPerGroup;
/// <summary>
/// The total number of pooled integers used by coefficient decoding.
/// </summary>
private const int ScratchLength = AbsoluteLevelsOffset + CoefficientsPerGroup;
/// <summary>
/// The allocator-owned scan and coefficient-group working storage reused for every transform block.
/// </summary>
private readonly IMemoryOwner<int> scratchOwner;
/// <summary>
/// The persistent Rice statistics for transformed and non-transformed luma and chroma blocks.
/// </summary>
private InlineArray4<int> riceAdaptationStatistics;
/// <summary>
/// Initializes a new instance of the <see cref="HevcCoefficientDecoder"/> class for one entropy substream.
/// </summary>
/// <param name="configuration">The configuration providing pooled codec memory.</param>
public HevcCoefficientDecoder(Configuration configuration)
{
this.scratchOwner = configuration.MemoryAllocator.Allocate<int>(ScratchLength);
this.riceAdaptationStatistics = default;
}
/// <summary>
/// Gets the minimum coordinate represented by each last-significant prefix.
/// </summary>
private static ReadOnlySpan<byte> MinimumCoordinateInGroup => [0, 1, 2, 3, 4, 6, 8, 12, 16, 24];
/// <summary>
/// Gets the last-significant prefix selected by each transform coordinate.
/// </summary>
private static ReadOnlySpan<byte> CoordinateGroupIndex =>
[
0, 1, 2, 3, 4, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7,
8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9,
];
/// <summary>
/// Clears all persistent Rice adaptation statistics for a newly initialized entropy substream.
/// </summary>
public void ResetRiceAdaptation() => this.riceAdaptationStatistics = default;
/// <summary>
/// Copies the four persistent Rice adaptation statistics to caller-owned wavefront state.
/// </summary>
/// <param name="destination">The four-element destination.</param>
public void CopyRiceAdaptationTo(Span<int> destination) => this.riceAdaptationStatistics[..4].CopyTo(destination);
/// <summary>
/// Restores the four persistent Rice adaptation statistics captured for a later wavefront row.
/// </summary>
/// <param name="source">The four saved statistics.</param>
public void CopyRiceAdaptationFrom(ReadOnlySpan<int> source) => source[..4].CopyTo(this.riceAdaptationStatistics[..4]);
/// <summary>
/// Decodes one transform block into raster-ordered signed coefficient levels.
/// </summary>
/// <param name="reader">The current entropy-substream syntax reader.</param>
/// <param name="coefficients">The destination coefficient block.</param>
/// <param name="parameters">The transform-block coefficient coding parameters.</param>
/// <returns>The number of nonzero coefficients decoded into <paramref name="coefficients"/>.</returns>
public int Decode(ref HevcCabacSyntaxReader reader, Span<int> coefficients, in HevcCoefficientCodingParameters parameters)
{
int width = parameters.Width;
int height = parameters.Height;
int coefficientCount = width * height;
bool isChroma = parameters.Plane != HevcPlane.Y;
coefficients[..coefficientCount].Clear();
ReadLastSignificantPosition(ref reader, in parameters, out int lastX, out int lastY);
int lastRasterPosition = (lastY * width) + lastX;
Span<int> scratch = this.scratchOwner.Memory.Span;
Span<int> scan = scratch[..coefficientCount];
int lastScanPosition = HevcCoefficientScanOrder.Write(scan, width, height, parameters.ScanType, lastRasterPosition);
int groupCount = coefficientCount / CoefficientsPerGroup;
Span<int> significantGroupFlags = scratch.Slice(CoefficientGroupFlagsOffset, groupCount);
Span<int> positions = scratch.Slice(CoefficientPositionsOffset, CoefficientsPerGroup);
Span<int> absoluteLevels = scratch.Slice(AbsoluteLevelsOffset, CoefficientsPerGroup);
significantGroupFlags.Clear();
int widthInGroups = width / 4;
int lastSubset = lastScanPosition / CoefficientsPerGroup;
int significantScanPosition = lastScanPosition;
int c1 = 1;
int totalNonZero = 0;
ref int currentRiceStatistic = ref this.riceAdaptationStatistics[parameters.RiceStatisticsIndex];
// Coefficient groups are decoded from the last significant position toward DC. This direction makes the
// already decoded right and lower groups available to the significance-context derivation below.
for (int subset = lastSubset; subset >= 0; subset--)
{
int subsetStart = subset * CoefficientsPerGroup;
int riceParameter = currentRiceStatistic / RiceAdaptationDivisor;
bool updateRiceStatistic = parameters.PersistentRiceAdaptationEnabled;
int nonZeroCount = 0;
int lastNonZeroScanPosition = -1;
int firstNonZeroScanPosition = CoefficientsPerGroup;
bool escapeDataPresent = false;
if (significantScanPosition == lastScanPosition)
{
lastNonZeroScanPosition = significantScanPosition;
firstNonZeroScanPosition = significantScanPosition;
significantScanPosition--;
positions[0] = lastRasterPosition;
nonZeroCount = 1;
}
int groupRasterPosition = scan[subsetStart];
int groupY = (groupRasterPosition / width) / 4;
int groupX = (groupRasterPosition % width) / 4;
int groupIndex = (groupY * widthInGroups) + groupX;
if (subset == lastSubset || subset == 0)
{
significantGroupFlags[groupIndex] = 1;
}
else
{
int groupContext = parameters.GetSignificantGroupContext(significantGroupFlags, groupX, groupY);
significantGroupFlags[groupIndex] = reader.ReadSignificantCoefficientGroup(isChroma, groupContext) ? 1 : 0;
}
int significancePattern = parameters.GetSignificancePattern(significantGroupFlags, groupX, groupY);
for (; significantScanPosition >= subsetStart; significantScanPosition--)
{
int rasterPosition = scan[significantScanPosition];
bool isSignificant = false;
if (significantGroupFlags[groupIndex] != 0)
{
if (significantScanPosition > subsetStart || subset == 0 || nonZeroCount != 0)
{
int contextIndex = parameters.GetSignificantCoefficientContext(rasterPosition, significancePattern);
isSignificant = reader.ReadSignificantCoefficient(isChroma, contextIndex);
}
else
{
// A coded significant group must contain at least one coefficient. When every later flag is
// zero, the first scan position is therefore inferred rather than consuming another CABAC bin.
isSignificant = true;
}
}
if (isSignificant)
{
positions[nonZeroCount++] = rasterPosition;
if (lastNonZeroScanPosition < 0)
{
lastNonZeroScanPosition = significantScanPosition;
}
firstNonZeroScanPosition = significantScanPosition;
}
}
if (nonZeroCount == 0)
{
continue;
}
bool hideSign = lastNonZeroScanPosition - firstNonZeroScanPosition >= SignDataHidingThreshold;
int contextSet = parameters.GetLevelContextSet(subset, c1 == 0);
c1 = 1;
absoluteLevels[..nonZeroCount].Fill(1);
int greaterThanOneCount = Math.Min(nonZeroCount, GreaterThanOneFlagCount);
int firstGreaterThanOneIndex = -1;
for (int index = 0; index < greaterThanOneCount; index++)
{
bool greaterThanOne = reader.ReadCoefficientGreaterThanOne(isChroma, (contextSet * 4) + c1);
if (greaterThanOne)
{
c1 = 0;
if (firstGreaterThanOneIndex < 0)
{
firstGreaterThanOneIndex = index;
}
else
{
escapeDataPresent = true;
}
}
else if (c1 is > 0 and < 3)
{
c1++;
}
absoluteLevels[index] = greaterThanOne ? 2 : 1;
}
if (c1 == 0 && firstGreaterThanOneIndex >= 0)
{
bool greaterThanTwo = reader.ReadCoefficientGreaterThanTwo(isChroma, contextSet);
absoluteLevels[firstGreaterThanOneIndex] = greaterThanTwo ? 3 : 2;
escapeDataPresent |= greaterThanTwo;
}
escapeDataPresent |= nonZeroCount > GreaterThanOneFlagCount;
if (escapeDataPresent && parameters.CabacBypassAlignmentEnabled)
{
reader.AlignBypass();
}
int signCount = hideSign && parameters.SignDataHidingEnabled ? nonZeroCount - 1 : nonZeroCount;
uint coefficientSigns = reader.ReadBypassBits(signCount);
int nextSignBit = signCount - 1;
int firstCoefficientAtLeastTwo = 1;
if (escapeDataPresent)
{
for (int index = 0; index < nonZeroCount; index++)
{
int baseLevel = index < GreaterThanOneFlagCount ? 2 + firstCoefficientAtLeastTwo : 1;
if (absoluteLevels[index] == baseLevel)
{
uint remainder = reader.ReadCoefficientRemaining(
riceParameter,
parameters.ExtendedPrecisionProcessingEnabled,
parameters.MaximumLog2TransformDynamicRange);
ulong decodedLevel = (ulong)remainder + (uint)baseLevel;
if (decodedLevel > int.MaxValue)
{
throw new InvalidImageContentException("The HEVC transform coefficient level is too large.");
}
absoluteLevels[index] = (int)decodedLevel;
if (decodedLevel > (3UL << riceParameter))
{
riceParameter = parameters.PersistentRiceAdaptationEnabled ? riceParameter + 1 : Math.Min(riceParameter + 1, 4);
}
if (updateRiceStatistic)
{
int initialRiceParameter = currentRiceStatistic / RiceAdaptationDivisor;
if (remainder >= (3UL << initialRiceParameter))
{
currentRiceStatistic++;
}
else if (((ulong)remainder * 2) < (1UL << initialRiceParameter) && currentRiceStatistic > 0)
{
currentRiceStatistic--;
}
// Only the first escape value in a coefficient group updates persistent state.
updateRiceStatistic = false;
}
}
if (absoluteLevels[index] >= 2)
{
firstCoefficientAtLeastTwo = 0;
}
}
}
int absoluteSum = 0;
for (int index = 0; index < nonZeroCount; index++)
{
int rasterPosition = positions[index];
int level = absoluteLevels[index];
absoluteSum += level;
if (index == nonZeroCount - 1 && hideSign && parameters.SignDataHidingEnabled)
{
level = (absoluteSum & 1) == 0 ? level : -level;
}
else if (((coefficientSigns >> nextSignBit--) & 1U) != 0)
{
level = -level;
}
coefficients[rasterPosition] = level;
}
totalNonZero += nonZeroCount;
}
return totalNonZero;
}
/// <summary>
/// Releases the allocator-owned coefficient scratch storage.
/// </summary>
public void Dispose() => this.scratchOwner.Dispose();
/// <summary>
/// Decodes the raster coordinates of the final significant coefficient.
/// </summary>
/// <param name="reader">The current entropy-substream syntax reader.</param>
/// <param name="parameters">The transform-block coefficient coding parameters.</param>
/// <param name="x">The decoded horizontal coordinate.</param>
/// <param name="y">The decoded vertical coordinate.</param>
private static void ReadLastSignificantPosition(
ref HevcCabacSyntaxReader reader,
in HevcCoefficientCodingParameters parameters,
out int x,
out int y)
{
bool verticalScan = parameters.ScanType == HevcCoefficientScanType.Vertical;
int syntaxWidth = verticalScan ? parameters.Height : parameters.Width;
int syntaxHeight = verticalScan ? parameters.Width : parameters.Height;
bool isChroma = parameters.Plane != HevcPlane.Y;
int xPrefix = ReadLastSignificantPrefix(ref reader, isChroma, syntaxWidth, true);
int yPrefix = ReadLastSignificantPrefix(ref reader, isChroma, syntaxHeight, false);
// The HEVC syntax carries both context-coded prefixes before either bypass-coded suffix. Decoding a suffix
// immediately after its own prefix changes the arithmetic bit order whenever both coordinates need suffixes.
x = ReadLastSignificantSuffix(ref reader, xPrefix);
y = ReadLastSignificantSuffix(ref reader, yPrefix);
if (verticalScan)
{
(x, y) = (y, x);
}
}
/// <summary>
/// Decodes one context-coded last-significant coefficient coordinate prefix.
/// </summary>
/// <param name="reader">The current entropy-substream syntax reader.</param>
/// <param name="isChroma">Whether the coordinate belongs to a chroma transform block.</param>
/// <param name="size">The transform-block extent along the coded axis.</param>
/// <param name="horizontal">Whether to use the horizontal rather than vertical context set.</param>
/// <returns>The decoded coordinate prefix.</returns>
private static int ReadLastSignificantPrefix(ref HevcCabacSyntaxReader reader, bool isChroma, int size, bool horizontal)
{
int convertedSize = BitOperations.Log2((uint)size) - 2;
int contextOffset = isChroma ? 0 : (convertedSize * 3) + ((convertedSize + 1) >> 2);
int contextShift = isChroma ? convertedSize : (convertedSize + 3) >> 2;
int maximumPrefix = CoordinateGroupIndex[size - 1];
int prefix;
for (prefix = 0; prefix < maximumPrefix; prefix++)
{
int contextIndex = contextOffset + (prefix >> contextShift);
bool prefixContinues = horizontal
? reader.ReadLastSignificantX(isChroma, contextIndex)
: reader.ReadLastSignificantY(isChroma, contextIndex);
if (!prefixContinues)
{
break;
}
}
return prefix;
}
/// <summary>
/// Expands one last-significant coordinate prefix with its bypass-coded suffix.
/// </summary>
/// <param name="reader">The current entropy-substream syntax reader.</param>
/// <param name="prefix">The context-coded coordinate prefix.</param>
/// <returns>The decoded zero-based coefficient coordinate.</returns>
private static int ReadLastSignificantSuffix(ref HevcCabacSyntaxReader reader, int prefix)
{
if (prefix <= 3)
{
return prefix;
}
int suffixLength = (prefix - 2) >> 1;
return MinimumCoordinateInGroup[prefix] + (int)reader.ReadBypassBits(suffixLength);
}
}

150
src/ImageSharp/Formats/Heif/Hevc/HevcCoefficientScanOrder.cs

@ -1,150 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Writes the grouped coefficient scan used by HEVC residual entropy coding.
/// </summary>
internal static class HevcCoefficientScanOrder
{
/// <summary>
/// The width and height of one coefficient group.
/// </summary>
private const int CoefficientGroupSize = 4;
/// <summary>
/// Writes the grouped scan for one transform block into caller-owned storage.
/// </summary>
/// <param name="destination">The destination receiving raster coefficient indices in scan order.</param>
/// <param name="width">The transform-block width.</param>
/// <param name="height">The transform-block height.</param>
/// <param name="scanType">The scan direction selected for the transform block.</param>
/// <param name="lastRasterPosition">The raster index of the last significant coefficient.</param>
/// <returns>The scan position of <paramref name="lastRasterPosition"/>.</returns>
public static int Write(Span<int> destination, int width, int height, HevcCoefficientScanType scanType, int lastRasterPosition)
{
int widthInGroups = width / CoefficientGroupSize;
int heightInGroups = height / CoefficientGroupSize;
int groupCount = widthInGroups * heightInGroups;
int lastScanPosition = -1;
ScanGenerator groupScan = new(widthInGroups, heightInGroups, scanType);
// H.265 scans the 4x4 groups first, then applies the same direction inside each group. Keeping this grouped
// layout contiguous lets coefficient decoding walk every 16-entry subset without lookup-table allocations.
for (int groupIndex = 0; groupIndex < groupCount; groupIndex++)
{
int groupOffsetX = groupScan.X * CoefficientGroupSize;
int groupOffsetY = groupScan.Y * CoefficientGroupSize;
int groupScanOffset = groupIndex * CoefficientGroupSize * CoefficientGroupSize;
ScanGenerator coefficientScan = new(CoefficientGroupSize, CoefficientGroupSize, scanType);
for (int coefficientIndex = 0; coefficientIndex < CoefficientGroupSize * CoefficientGroupSize; coefficientIndex++)
{
int rasterPosition = ((groupOffsetY + coefficientScan.Y) * width) + groupOffsetX + coefficientScan.X;
int scanPosition = groupScanOffset + coefficientIndex;
destination[scanPosition] = rasterPosition;
if (rasterPosition == lastRasterPosition)
{
lastScanPosition = scanPosition;
}
coefficientScan.MoveNext();
}
groupScan.MoveNext();
}
return lastScanPosition;
}
/// <summary>
/// Advances through one rectangular scan without retaining a heap-backed lookup table.
/// </summary>
private struct ScanGenerator
{
/// <summary>
/// The scan width.
/// </summary>
private readonly int width;
/// <summary>
/// The scan height.
/// </summary>
private readonly int height;
/// <summary>
/// The selected scan direction.
/// </summary>
private readonly HevcCoefficientScanType scanType;
/// <summary>
/// Initializes a new instance of the <see cref="ScanGenerator"/> struct.
/// </summary>
/// <param name="width">The scan width.</param>
/// <param name="height">The scan height.</param>
/// <param name="scanType">The scan direction.</param>
public ScanGenerator(int width, int height, HevcCoefficientScanType scanType)
{
this.width = width;
this.height = height;
this.scanType = scanType;
this.X = 0;
this.Y = 0;
}
/// <summary>
/// Gets the current horizontal coordinate.
/// </summary>
public int X { get; private set; }
/// <summary>
/// Gets the current vertical coordinate.
/// </summary>
public int Y { get; private set; }
/// <summary>
/// Advances to the next coordinate in the selected scan direction.
/// </summary>
public void MoveNext()
{
switch (this.scanType)
{
case HevcCoefficientScanType.Diagonal:
if (this.X == this.width - 1 || this.Y == 0)
{
this.Y += this.X + 1;
this.X = 0;
if (this.Y >= this.height)
{
this.X += this.Y - (this.height - 1);
this.Y = this.height - 1;
}
}
else
{
this.X++;
this.Y--;
}
break;
case HevcCoefficientScanType.Horizontal:
if (++this.X == this.width)
{
this.X = 0;
this.Y++;
}
break;
default:
if (++this.Y == this.height)
{
this.Y = 0;
this.X++;
}
break;
}
}
}
}

25
src/ImageSharp/Formats/Heif/Hevc/HevcCoefficientScanType.cs

@ -1,25 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Identifies the coefficient scan used by one HEVC transform block.
/// </summary>
internal enum HevcCoefficientScanType
{
/// <summary>
/// The up-right diagonal scan.
/// </summary>
Diagonal,
/// <summary>
/// The row-major horizontal scan.
/// </summary>
Horizontal,
/// <summary>
/// The column-major vertical scan.
/// </summary>
Vertical,
}

64
src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingFilter.HorizontalEdgeOperator.cs

@ -1,64 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
internal static partial class HevcDeblockingFilter
{
/// <summary>
/// Accesses four columns across a horizontal edge.
/// </summary>
private readonly struct HorizontalEdgeOperator : IEdgeOperator
{
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<int> LoadVector(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int count)
{
ref ushort source = ref picture.GetRowSpan(plane, y + distance)[x];
if (count == 2)
{
// The packed load used by full-width segments would read two samples beyond a subsampled edge.
return Vector128.Create((int)source, Unsafe.Add(ref source, 1), 0, 0);
}
Vector64<ushort> packed = Unsafe.As<ushort, Vector64<ushort>>(ref source);
return Vector128.WidenLower(Vector128.Create(packed, Vector64<ushort>.Zero)).AsInt32();
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void StoreVector(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int distance,
Vector128<int> value,
int count)
{
ref ushort destination = ref picture.GetRowSpan(plane, y + distance)[x];
if (count == 4)
{
Vector64<ushort> packed = Vector128.Narrow(value, Vector128<int>.Zero).AsUInt16().GetLower();
Unsafe.As<ushort, Vector64<ushort>>(ref destination) = packed;
return;
}
destination = (ushort)value.GetElement(0);
Unsafe.Add(ref destination, 1) = (ushort)value.GetElement(1);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int LoadScalar(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int index)
=> picture.GetRowSpan(plane, y + distance)[x + index];
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void StoreScalar(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int index, int value)
=> picture.GetRowSpan(plane, y + distance)[x + index] = (ushort)value;
}
}

70
src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingFilter.Operator.cs

@ -1,70 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
internal static partial class HevcDeblockingFilter
{
/// <summary>
/// Defines orientation-specific access to the four samples running along one deblocking edge segment.
/// </summary>
private interface IEdgeOperator
{
/// <summary>
/// Loads samples at one signed distance across the edge.
/// </summary>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="distance">The signed sample distance across the edge.</param>
/// <param name="count">The number of valid low lanes to load.</param>
/// <returns>The widened samples ordered along the edge.</returns>
public static abstract Vector128<int> LoadVector(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int count);
/// <summary>
/// Stores four samples at one signed distance across the edge.
/// </summary>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="distance">The signed sample distance across the edge.</param>
/// <param name="value">The four widened samples ordered along the edge.</param>
/// <param name="count">The number of low lanes to store.</param>
public static abstract void StoreVector(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int distance,
Vector128<int> value,
int count);
/// <summary>
/// Loads one scalar sample at a signed distance across and an offset along the edge.
/// </summary>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="distance">The signed sample distance across the edge.</param>
/// <param name="index">The sample offset along the edge.</param>
/// <returns>The selected sample.</returns>
public static abstract int LoadScalar(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int index);
/// <summary>
/// Stores one scalar sample at a signed distance across and an offset along the edge.
/// </summary>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="distance">The signed sample distance across the edge.</param>
/// <param name="index">The sample offset along the edge.</param>
/// <param name="value">The filtered sample.</param>
public static abstract void StoreScalar(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int index, int value);
}
}

65
src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingFilter.VerticalEdgeOperator.cs

@ -1,65 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
internal static partial class HevcDeblockingFilter
{
/// <summary>
/// Accesses four rows across a vertical edge.
/// </summary>
private readonly struct VerticalEdgeOperator : IEdgeOperator
{
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<int> LoadVector(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int count)
{
if (count == 4)
{
return Vector128.Create(
(int)picture.GetRowSpan(plane, y)[x + distance],
picture.GetRowSpan(plane, y + 1)[x + distance],
picture.GetRowSpan(plane, y + 2)[x + distance],
picture.GetRowSpan(plane, y + 3)[x + distance]);
}
// Subsampled chroma edges contain two samples. Zeroing the unused lanes keeps the vector path within
// the plane while allowing the shared kernel to operate on both valid samples in one instruction stream.
return Vector128.Create(
(int)picture.GetRowSpan(plane, y)[x + distance],
picture.GetRowSpan(plane, y + 1)[x + distance],
0,
0);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void StoreVector(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int distance,
Vector128<int> value,
int count)
{
for (int index = 0; index < count; index++)
{
picture.GetRowSpan(plane, y + index)[x + distance] = (ushort)value.GetElement(index);
}
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int LoadScalar(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int index)
=> picture.GetRowSpan(plane, y + index)[x + distance];
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void StoreScalar(HevcPictureBuffer picture, HevcPlane plane, int x, int y, int distance, int index, int value)
=> picture.GetRowSpan(plane, y + index)[x + distance] = (ushort)value;
}
}

470
src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingFilter.cs

@ -1,470 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Applies the HEVC luma and chroma deblocking kernels to four-sample edge segments.
/// </summary>
/// <remarks>
/// Each 32-bit lane represents one position along an edge segment. Orientation-specific operators gather the samples
/// at a common signed distance across that edge; the luma and chroma masks and adjustments then execute lane-wise.
/// Loads with fewer than four valid positions populate only the low lanes, which the matching store writes without
/// touching samples beyond the picture boundary.
/// </remarks>
internal static partial class HevcDeblockingFilter
{
/// <summary>
/// Filters four rows crossing one vertical luma boundary.
/// </summary>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The top sample Y coordinate.</param>
/// <param name="beta">The scaled discontinuity threshold.</param>
/// <param name="tc">The scaled clipping threshold.</param>
/// <param name="partPNoFilter">Whether the P-side block retains its original samples.</param>
/// <param name="partQNoFilter">Whether the Q-side block retains its original samples.</param>
/// <param name="bitDepth">The component sample precision.</param>
public static void FilterVerticalLuma(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int beta,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth)
=> FilterLuma<VerticalEdgeOperator>(picture, plane, x, y, beta, tc, partPNoFilter, partQNoFilter, bitDepth);
/// <summary>
/// Filters four columns crossing one horizontal luma boundary.
/// </summary>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The left sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="beta">The scaled discontinuity threshold.</param>
/// <param name="tc">The scaled clipping threshold.</param>
/// <param name="partPNoFilter">Whether the P-side block retains its original samples.</param>
/// <param name="partQNoFilter">Whether the Q-side block retains its original samples.</param>
/// <param name="bitDepth">The component sample precision.</param>
public static void FilterHorizontalLuma(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int beta,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth)
=> FilterLuma<HorizontalEdgeOperator>(picture, plane, x, y, beta, tc, partPNoFilter, partQNoFilter, bitDepth);
/// <summary>
/// Filters four rows crossing one vertical chroma boundary.
/// </summary>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The Cb or Cr component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The top sample Y coordinate.</param>
/// <param name="tc">The scaled clipping threshold.</param>
/// <param name="partPNoFilter">Whether the P-side block retains its original samples.</param>
/// <param name="partQNoFilter">Whether the Q-side block retains its original samples.</param>
/// <param name="bitDepth">The component sample precision.</param>
/// <param name="count">The number of samples in the edge segment.</param>
public static void FilterVerticalChroma(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth,
int count)
=> FilterChroma<VerticalEdgeOperator>(picture, plane, x, y, tc, partPNoFilter, partQNoFilter, bitDepth, count);
/// <summary>
/// Filters four columns crossing one horizontal chroma boundary.
/// </summary>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The Cb or Cr component plane.</param>
/// <param name="x">The left sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="tc">The scaled clipping threshold.</param>
/// <param name="partPNoFilter">Whether the P-side block retains its original samples.</param>
/// <param name="partQNoFilter">Whether the Q-side block retains its original samples.</param>
/// <param name="bitDepth">The component sample precision.</param>
/// <param name="count">The number of samples in the edge segment.</param>
public static void FilterHorizontalChroma(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth,
int count)
=> FilterChroma<HorizontalEdgeOperator>(picture, plane, x, y, tc, partPNoFilter, partQNoFilter, bitDepth, count);
/// <summary>
/// Applies the strong or weak luma kernel through one closed edge-orientation operator.
/// </summary>
/// <typeparam name="TOperator">The vertical or horizontal sample-access operator.</typeparam>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="beta">The scaled discontinuity threshold.</param>
/// <param name="tc">The scaled clipping threshold.</param>
/// <param name="partPNoFilter">Whether the P-side block retains its original samples.</param>
/// <param name="partQNoFilter">Whether the Q-side block retains its original samples.</param>
/// <param name="bitDepth">The component sample precision.</param>
private static void FilterLuma<TOperator>(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int beta,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth)
where TOperator : struct, IEdgeOperator
{
if (beta == 0)
{
return;
}
int p2Start = TOperator.LoadScalar(picture, plane, x, y, -3, 0);
int p1Start = TOperator.LoadScalar(picture, plane, x, y, -2, 0);
int p0Start = TOperator.LoadScalar(picture, plane, x, y, -1, 0);
int q0Start = TOperator.LoadScalar(picture, plane, x, y, 0, 0);
int q1Start = TOperator.LoadScalar(picture, plane, x, y, 1, 0);
int q2Start = TOperator.LoadScalar(picture, plane, x, y, 2, 0);
int p2End = TOperator.LoadScalar(picture, plane, x, y, -3, 3);
int p1End = TOperator.LoadScalar(picture, plane, x, y, -2, 3);
int p0End = TOperator.LoadScalar(picture, plane, x, y, -1, 3);
int q0End = TOperator.LoadScalar(picture, plane, x, y, 0, 3);
int q1End = TOperator.LoadScalar(picture, plane, x, y, 1, 3);
int q2End = TOperator.LoadScalar(picture, plane, x, y, 2, 3);
int dpStart = Math.Abs(p2Start - (2 * p1Start) + p0Start);
int dqStart = Math.Abs(q0Start - (2 * q1Start) + q2Start);
int dpEnd = Math.Abs(p2End - (2 * p1End) + p0End);
int dqEnd = Math.Abs(q0End - (2 * q1End) + q2End);
int dp = dpStart + dpEnd;
int dq = dqStart + dqEnd;
int discontinuity = dp + dq;
if (discontinuity >= beta)
{
return;
}
int sideThreshold = (beta + (beta >> 1)) >> 3;
bool filterSecondP = dp < sideThreshold;
bool filterSecondQ = dq < sideThreshold;
bool strong = UsesStrongFiltering<TOperator>(picture, plane, x, y, 0, 2 * (dpStart + dqStart), beta, tc)
&& UsesStrongFiltering<TOperator>(picture, plane, x, y, 3, 2 * (dpEnd + dqEnd), beta, tc);
if (!Vector128.IsHardwareAccelerated)
{
for (int index = 0; index < 4; index++)
{
FilterLumaScalar<TOperator>(
picture,
plane,
x,
y,
index,
tc,
strong,
partPNoFilter,
partQNoFilter,
tc * 10,
filterSecondP,
filterSecondQ,
bitDepth);
}
return;
}
Vector128<int> p3 = TOperator.LoadVector(picture, plane, x, y, -4, 4);
Vector128<int> p2 = TOperator.LoadVector(picture, plane, x, y, -3, 4);
Vector128<int> p1 = TOperator.LoadVector(picture, plane, x, y, -2, 4);
Vector128<int> p0 = TOperator.LoadVector(picture, plane, x, y, -1, 4);
Vector128<int> q0 = TOperator.LoadVector(picture, plane, x, y, 0, 4);
Vector128<int> q1 = TOperator.LoadVector(picture, plane, x, y, 1, 4);
Vector128<int> q2 = TOperator.LoadVector(picture, plane, x, y, 2, 4);
Vector128<int> q3 = TOperator.LoadVector(picture, plane, x, y, 3, 4);
// Each Int32 lane is one row or column along the edge. The threshold decision is shared by all four lanes,
// while the filter arithmetic stays lane-local and exactly matches the scalar equations below.
if (strong)
{
Vector128<int> twiceTc = Vector128.Create(2 * tc);
Vector128<int> four = Vector128.Create(4);
Vector128<int> two = Vector128.Create(2);
Vector128<int> filteredP0 = Vector128.Clamp((p2 + (p1 * 2) + (p0 * 2) + (q0 * 2) + q1 + four) >> 3, p0 - twiceTc, p0 + twiceTc);
Vector128<int> filteredQ0 = Vector128.Clamp((p1 + (p0 * 2) + (q0 * 2) + (q1 * 2) + q2 + four) >> 3, q0 - twiceTc, q0 + twiceTc);
Vector128<int> filteredP1 = Vector128.Clamp((p2 + p1 + p0 + q0 + two) >> 2, p1 - twiceTc, p1 + twiceTc);
Vector128<int> filteredQ1 = Vector128.Clamp((p0 + q0 + q1 + q2 + two) >> 2, q1 - twiceTc, q1 + twiceTc);
Vector128<int> filteredP2 = Vector128.Clamp(((p3 * 2) + (p2 * 3) + p1 + p0 + q0 + four) >> 3, p2 - twiceTc, p2 + twiceTc);
Vector128<int> filteredQ2 = Vector128.Clamp((p0 + q0 + q1 + (q2 * 3) + (q3 * 2) + four) >> 3, q2 - twiceTc, q2 + twiceTc);
TOperator.StoreVector(picture, plane, x, y, -3, partPNoFilter ? p2 : filteredP2, 4);
TOperator.StoreVector(picture, plane, x, y, -2, partPNoFilter ? p1 : filteredP1, 4);
TOperator.StoreVector(picture, plane, x, y, -1, partPNoFilter ? p0 : filteredP0, 4);
TOperator.StoreVector(picture, plane, x, y, 0, partQNoFilter ? q0 : filteredQ0, 4);
TOperator.StoreVector(picture, plane, x, y, 1, partQNoFilter ? q1 : filteredQ1, 4);
TOperator.StoreVector(picture, plane, x, y, 2, partQNoFilter ? q2 : filteredQ2, 4);
return;
}
Vector128<int> primaryDifference = (q0 - p0) * 9;
Vector128<int> secondaryDifference = (q1 - p1) * 3;
Vector128<int> delta = (primaryDifference - secondaryDifference + Vector128.Create(8)) >> 4;
Vector128<int> filterMask = Vector128.LessThan(Vector128.Abs(delta), Vector128.Create(tc * 10));
delta = Vector128.Clamp(delta, Vector128.Create(-tc), Vector128.Create(tc));
Vector128<int> minimum = Vector128<int>.Zero;
Vector128<int> maximum = Vector128.Create((1 << bitDepth) - 1);
Vector128<int> filteredP0Weak = Vector128.ConditionalSelect(filterMask, Vector128.Clamp(p0 + delta, minimum, maximum), p0);
Vector128<int> filteredQ0Weak = Vector128.ConditionalSelect(filterMask, Vector128.Clamp(q0 - delta, minimum, maximum), q0);
TOperator.StoreVector(picture, plane, x, y, -1, partPNoFilter ? p0 : filteredP0Weak, 4);
TOperator.StoreVector(picture, plane, x, y, 0, partQNoFilter ? q0 : filteredQ0Weak, 4);
int halfTc = tc >> 1;
if (filterSecondP && !partPNoFilter)
{
Vector128<int> secondary = (((p2 + p0 + Vector128<int>.One) >> 1) - p1 + delta) >> 1;
secondary = Vector128.Clamp(secondary, Vector128.Create(-halfTc), Vector128.Create(halfTc));
Vector128<int> filtered = Vector128.ConditionalSelect(filterMask, Vector128.Clamp(p1 + secondary, minimum, maximum), p1);
TOperator.StoreVector(picture, plane, x, y, -2, filtered, 4);
}
if (filterSecondQ && !partQNoFilter)
{
Vector128<int> secondary = (((q2 + q0 + Vector128<int>.One) >> 1) - q1 - delta) >> 1;
secondary = Vector128.Clamp(secondary, Vector128.Create(-halfTc), Vector128.Create(halfTc));
Vector128<int> filtered = Vector128.ConditionalSelect(filterMask, Vector128.Clamp(q1 + secondary, minimum, maximum), q1);
TOperator.StoreVector(picture, plane, x, y, 1, filtered, 4);
}
}
/// <summary>
/// Applies the chroma kernel through one closed edge-orientation operator.
/// </summary>
/// <typeparam name="TOperator">The vertical or horizontal sample-access operator.</typeparam>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The Cb or Cr component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="tc">The scaled clipping threshold.</param>
/// <param name="partPNoFilter">Whether the P-side block retains its original samples.</param>
/// <param name="partQNoFilter">Whether the Q-side block retains its original samples.</param>
/// <param name="bitDepth">The component sample precision.</param>
/// <param name="count">The number of samples in the edge segment.</param>
private static void FilterChroma<TOperator>(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth,
int count)
where TOperator : struct, IEdgeOperator
{
if (tc == 0)
{
return;
}
if (!Vector128.IsHardwareAccelerated)
{
int maximum = (1 << bitDepth) - 1;
for (int index = 0; index < count; index++)
{
int p1 = TOperator.LoadScalar(picture, plane, x, y, -2, index);
int p0 = TOperator.LoadScalar(picture, plane, x, y, -1, index);
int q0 = TOperator.LoadScalar(picture, plane, x, y, 0, index);
int q1 = TOperator.LoadScalar(picture, plane, x, y, 1, index);
int delta = Math.Clamp((((q0 - p0) << 2) + p1 - q1 + 4) >> 3, -tc, tc);
if (!partPNoFilter)
{
TOperator.StoreScalar(picture, plane, x, y, -1, index, Math.Clamp(p0 + delta, 0, maximum));
}
if (!partQNoFilter)
{
TOperator.StoreScalar(picture, plane, x, y, 0, index, Math.Clamp(q0 - delta, 0, maximum));
}
}
return;
}
Vector128<int> p1Vector = TOperator.LoadVector(picture, plane, x, y, -2, count);
Vector128<int> p0Vector = TOperator.LoadVector(picture, plane, x, y, -1, count);
Vector128<int> q0Vector = TOperator.LoadVector(picture, plane, x, y, 0, count);
Vector128<int> q1Vector = TOperator.LoadVector(picture, plane, x, y, 1, count);
Vector128<int> deltaVector = (((q0Vector - p0Vector) * 4) + p1Vector - q1Vector + Vector128.Create(4)) >> 3;
deltaVector = Vector128.Clamp(deltaVector, Vector128.Create(-tc), Vector128.Create(tc));
Vector128<int> minimum = Vector128<int>.Zero;
Vector128<int> maximumVector = Vector128.Create((1 << bitDepth) - 1);
if (!partPNoFilter)
{
TOperator.StoreVector(picture, plane, x, y, -1, Vector128.Clamp(p0Vector + deltaVector, minimum, maximumVector), count);
}
if (!partQNoFilter)
{
TOperator.StoreVector(picture, plane, x, y, 0, Vector128.Clamp(q0Vector - deltaVector, minimum, maximumVector), count);
}
}
/// <summary>
/// Applies the scalar luma equations to one sample along an edge.
/// </summary>
/// <typeparam name="TOperator">The vertical or horizontal sample-access operator.</typeparam>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="index">The sample offset along the edge.</param>
/// <param name="tc">The scaled clipping threshold.</param>
/// <param name="strong">Whether the strong six-sample filter is selected.</param>
/// <param name="partPNoFilter">Whether the P-side block retains its original samples.</param>
/// <param name="partQNoFilter">Whether the Q-side block retains its original samples.</param>
/// <param name="thresholdCut">The weak-filter delta threshold.</param>
/// <param name="filterSecondP">Whether the second P-side sample is filtered.</param>
/// <param name="filterSecondQ">Whether the second Q-side sample is filtered.</param>
/// <param name="bitDepth">The component sample precision.</param>
private static void FilterLumaScalar<TOperator>(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int index,
int tc,
bool strong,
bool partPNoFilter,
bool partQNoFilter,
int thresholdCut,
bool filterSecondP,
bool filterSecondQ,
int bitDepth)
where TOperator : struct, IEdgeOperator
{
int p3 = TOperator.LoadScalar(picture, plane, x, y, -4, index);
int p2 = TOperator.LoadScalar(picture, plane, x, y, -3, index);
int p1 = TOperator.LoadScalar(picture, plane, x, y, -2, index);
int p0 = TOperator.LoadScalar(picture, plane, x, y, -1, index);
int q0 = TOperator.LoadScalar(picture, plane, x, y, 0, index);
int q1 = TOperator.LoadScalar(picture, plane, x, y, 1, index);
int q2 = TOperator.LoadScalar(picture, plane, x, y, 2, index);
int q3 = TOperator.LoadScalar(picture, plane, x, y, 3, index);
if (strong)
{
if (!partPNoFilter)
{
int filteredP0 = Math.Clamp(
(p2 + (2 * p1) + (2 * p0) + (2 * q0) + q1 + 4) >> 3,
p0 - (2 * tc),
p0 + (2 * tc));
TOperator.StoreScalar(picture, plane, x, y, -1, index, filteredP0);
TOperator.StoreScalar(picture, plane, x, y, -2, index, Math.Clamp((p2 + p1 + p0 + q0 + 2) >> 2, p1 - (2 * tc), p1 + (2 * tc)));
TOperator.StoreScalar(picture, plane, x, y, -3, index, Math.Clamp(((2 * p3) + (3 * p2) + p1 + p0 + q0 + 4) >> 3, p2 - (2 * tc), p2 + (2 * tc)));
}
if (!partQNoFilter)
{
int filteredQ0 = Math.Clamp(
(p1 + (2 * p0) + (2 * q0) + (2 * q1) + q2 + 4) >> 3,
q0 - (2 * tc),
q0 + (2 * tc));
TOperator.StoreScalar(picture, plane, x, y, 0, index, filteredQ0);
TOperator.StoreScalar(picture, plane, x, y, 1, index, Math.Clamp((p0 + q0 + q1 + q2 + 2) >> 2, q1 - (2 * tc), q1 + (2 * tc)));
TOperator.StoreScalar(picture, plane, x, y, 2, index, Math.Clamp((p0 + q0 + q1 + (3 * q2) + (2 * q3) + 4) >> 3, q2 - (2 * tc), q2 + (2 * tc)));
}
return;
}
int delta = ((9 * (q0 - p0)) - (3 * (q1 - p1)) + 8) >> 4;
if (Math.Abs(delta) >= thresholdCut)
{
return;
}
delta = Math.Clamp(delta, -tc, tc);
int maximum = (1 << bitDepth) - 1;
if (!partPNoFilter)
{
TOperator.StoreScalar(picture, plane, x, y, -1, index, Math.Clamp(p0 + delta, 0, maximum));
if (filterSecondP)
{
int secondary = (((p2 + p0 + 1) >> 1) - p1 + delta) >> 1;
secondary = Math.Clamp(secondary, -(tc >> 1), tc >> 1);
TOperator.StoreScalar(picture, plane, x, y, -2, index, Math.Clamp(p1 + secondary, 0, maximum));
}
}
if (!partQNoFilter)
{
TOperator.StoreScalar(picture, plane, x, y, 0, index, Math.Clamp(q0 - delta, 0, maximum));
if (filterSecondQ)
{
int secondary = (((q2 + q0 + 1) >> 1) - q1 - delta) >> 1;
secondary = Math.Clamp(secondary, -(tc >> 1), tc >> 1);
TOperator.StoreScalar(picture, plane, x, y, 1, index, Math.Clamp(q1 + secondary, 0, maximum));
}
}
}
/// <summary>
/// Determines whether one endpoint satisfies the strong-filter conditions.
/// </summary>
/// <typeparam name="TOperator">The vertical or horizontal sample-access operator.</typeparam>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="index">The endpoint offset along the edge.</param>
/// <param name="discontinuity">Twice the endpoint's second-derivative sum.</param>
/// <param name="beta">The scaled discontinuity threshold.</param>
/// <param name="tc">The scaled clipping threshold.</param>
/// <returns><see langword="true"/> when strong filtering is permitted; otherwise, <see langword="false"/>.</returns>
private static bool UsesStrongFiltering<TOperator>(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int index,
int discontinuity,
int beta,
int tc)
where TOperator : struct, IEdgeOperator
{
int p3 = TOperator.LoadScalar(picture, plane, x, y, -4, index);
int p0 = TOperator.LoadScalar(picture, plane, x, y, -1, index);
int q0 = TOperator.LoadScalar(picture, plane, x, y, 0, index);
int q3 = TOperator.LoadScalar(picture, plane, x, y, 3, index);
int strongDiscontinuity = Math.Abs(p3 - p0) + Math.Abs(q3 - q0);
int strongThreshold = ((5 * tc) + 1) >> 1;
return strongDiscontinuity < (beta >> 3)
&& discontinuity < (beta >> 2)
&& Math.Abs(p0 - q0) < strongThreshold;
}
}

131
src/ImageSharp/Formats/Heif/Hevc/HevcDeblockingState.cs

@ -1,131 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Tracks luma transform and prediction boundaries at the four-sample resolution used to derive HEVC deblocking edges.
/// </summary>
internal sealed class HevcDeblockingState : IDisposable
{
/// <summary>
/// The base-two logarithm of the boundary-map unit side.
/// </summary>
private const int UnitLog2 = 2;
/// <summary>
/// The packed flag identifying a vertical boundary at a unit's left edge.
/// </summary>
private const byte VerticalBoundary = 1 << 0;
/// <summary>
/// The packed flag identifying a horizontal boundary at a unit's top edge.
/// </summary>
private const byte HorizontalBoundary = 1 << 1;
/// <summary>
/// The boundary maps for the primary plane of combined coding or each independently coded color plane.
/// </summary>
private readonly Buffer2D<byte>[] boundaries;
/// <summary>
/// Initializes a new instance of the <see cref="HevcDeblockingState"/> class.
/// </summary>
/// <param name="configuration">The configuration providing the image memory allocator.</param>
/// <param name="sequenceParameterSet">The coded picture dimensions.</param>
public HevcDeblockingState(Configuration configuration, HevcSequenceParameterSet sequenceParameterSet)
{
int width = DivideCeilingByPowerOfTwo(sequenceParameterSet.Width, UnitLog2);
int height = DivideCeilingByPowerOfTwo(sequenceParameterSet.Height, UnitLog2);
Buffer2D<byte>? lumaBoundaries = null;
Buffer2D<byte>? chromaBlueBoundaries = null;
Buffer2D<byte>? chromaRedBoundaries = null;
try
{
// MarkBlock combines sparse edge flags with existing values, so zero initialization is part of the state contract.
lumaBoundaries = configuration.MemoryAllocator.Allocate2D<byte>(width, height, AllocationOptions.Clean);
chromaBlueBoundaries = configuration.MemoryAllocator.Allocate2D<byte>(width, height, AllocationOptions.Clean);
chromaRedBoundaries = configuration.MemoryAllocator.Allocate2D<byte>(width, height, AllocationOptions.Clean);
this.boundaries = [lumaBoundaries, chromaBlueBoundaries, chromaRedBoundaries];
}
catch
{
chromaRedBoundaries?.Dispose();
chromaBlueBoundaries?.Dispose();
lumaBoundaries?.Dispose();
throw;
}
}
/// <summary>
/// Records the left and top edges of one leaf transform or pulse-code-modulated coding block.
/// </summary>
/// <param name="plane">The primary coding plane.</param>
/// <param name="x">The block left coordinate in full-resolution primary-plane samples.</param>
/// <param name="y">The block top coordinate in full-resolution primary-plane samples.</param>
/// <param name="width">The block width in samples.</param>
/// <param name="height">The block height in samples.</param>
public void MarkBlock(HevcPlane plane, int x, int y, int width, int height)
{
Buffer2D<byte> map = this.boundaries[(int)plane];
int unitX = x >> UnitLog2;
int unitY = y >> UnitLog2;
int endX = Math.Min(DivideCeilingByPowerOfTwo(x + width, UnitLog2), map.Width);
int endY = Math.Min(DivideCeilingByPowerOfTwo(y + height, UnitLog2), map.Height);
// A transform boundary covers every four-sample segment along its edge. Packing both orientations into one
// byte keeps the decoder state contiguous and lets the later eight-sample deblocking traversal reject edges cheaply.
for (int row = unitY; row < endY; row++)
{
map.DangerousGetRowSpan(row)[unitX] |= VerticalBoundary;
}
Span<byte> top = map.DangerousGetRowSpan(unitY);
for (int column = unitX; column < endX; column++)
{
top[column] |= HorizontalBoundary;
}
}
/// <summary>
/// Gets whether a four-sample segment begins at a vertical transform or prediction boundary.
/// </summary>
/// <param name="plane">The primary coding plane.</param>
/// <param name="x">The segment left coordinate in full-resolution primary-plane samples.</param>
/// <param name="y">The segment top coordinate in full-resolution primary-plane samples.</param>
/// <returns><see langword="true"/> when the segment is a vertical boundary; otherwise, <see langword="false"/>.</returns>
public bool IsVerticalBoundary(HevcPlane plane, int x, int y)
=> (this.boundaries[(int)plane].DangerousGetRowSpan(y >> UnitLog2)[x >> UnitLog2] & VerticalBoundary) != 0;
/// <summary>
/// Gets whether a four-sample segment begins at a horizontal transform or prediction boundary.
/// </summary>
/// <param name="plane">The primary coding plane.</param>
/// <param name="x">The segment left coordinate in full-resolution primary-plane samples.</param>
/// <param name="y">The segment top coordinate in full-resolution primary-plane samples.</param>
/// <returns><see langword="true"/> when the segment is a horizontal boundary; otherwise, <see langword="false"/>.</returns>
public bool IsHorizontalBoundary(HevcPlane plane, int x, int y)
=> (this.boundaries[(int)plane].DangerousGetRowSpan(y >> UnitLog2)[x >> UnitLog2] & HorizontalBoundary) != 0;
/// <summary>
/// Releases the allocator-owned boundary maps.
/// </summary>
public void Dispose()
{
foreach (Buffer2D<byte> map in this.boundaries)
{
map.Dispose();
}
}
/// <summary>
/// Divides a nonnegative sample count by a power of two with upward rounding.
/// </summary>
/// <param name="value">The sample count.</param>
/// <param name="shift">The base-two divisor logarithm.</param>
/// <returns>The upward-rounded quotient.</returns>
private static int DivideCeilingByPowerOfTwo(int value, int shift) => (value + (1 << shift) - 1) >> shift;
}

144
src/ImageSharp/Formats/Heif/Hevc/HevcImageItemBitstream.cs

@ -1,144 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Contains the length-delimited NAL units and IDR slice segments carried by one HEVC still-image item.
/// </summary>
internal sealed class HevcImageItemBitstream
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcImageItemBitstream"/> class.
/// </summary>
/// <param name="data">The complete bounded payload of one <c>hvc1</c> image item.</param>
/// <param name="configuration">The codec configuration associated with the same image item.</param>
/// <exception cref="InvalidImageContentException">
/// NAL-unit framing is malformed, the payload contains sequence or layered coding, or the item does not contain
/// exactly one independently decodable IDR picture.
/// </exception>
public HevcImageItemBitstream(ReadOnlySpan<byte> data, HevcCodecConfiguration configuration)
{
List<HevcNalUnit> nalUnits = [];
List<HevcSliceSegmentHeader> sliceSegments = [];
HevcSupplementalEnhancementInformation supplementalEnhancementInformation = new();
int offset = 0;
while (offset < data.Length)
{
if (data.Length - offset < configuration.NalUnitLengthSize)
{
throw new InvalidImageContentException("The HEVC image item has a truncated NAL-unit length.");
}
int nalUnitLength = ReadNalUnitLength(data[offset..], configuration.NalUnitLengthSize);
offset += configuration.NalUnitLengthSize;
if (nalUnitLength < 2 || nalUnitLength > data.Length - offset)
{
throw new InvalidImageContentException("The HEVC image item has an invalid NAL-unit length.");
}
HevcNalUnit nalUnit = new(data.Slice(offset, nalUnitLength));
offset += nalUnitLength;
if (nalUnit.Header.LayerId != 0 || nalUnit.Header.TemporalId != 0)
{
throw new InvalidImageContentException("The HEVC image item contains layered or temporal-substream NAL units.");
}
nalUnits.Add(nalUnit);
if (nalUnit.Header.IsVideoCodingLayer)
{
if (!nalUnit.Header.IsInstantaneousDecoderRefresh)
{
throw new InvalidImageContentException("The HEVC image item contains a coded picture that is not independently decodable.");
}
HevcSliceSegmentHeader sliceSegment = new(nalUnit, configuration.PictureParameterSets);
if (sliceSegments.Count == 0 && !sliceSegment.FirstSliceSegmentInPicture)
{
throw new InvalidImageContentException("The first HEVC image-item slice is not marked as the first picture segment.");
}
if (sliceSegments.Count != 0 && sliceSegment.FirstSliceSegmentInPicture)
{
throw new InvalidImageContentException("The HEVC image item contains more than one coded picture.");
}
sliceSegments.Add(sliceSegment);
continue;
}
if (nalUnit.Header.NalUnitType is 32 or 33 or 34)
{
// hvc1 image items obtain all parameter sets from the associated hvcC property. Accepting in-band
// replacements would silently apply the more permissive hev1 sample contract to this still image.
throw new InvalidImageContentException("The HEVC hvc1 image item contains an in-band parameter set.");
}
if (nalUnit.Header.NalUnitType is 36 or 37)
{
throw new InvalidImageContentException("The HEVC image item contains an end-of-sequence NAL unit.");
}
if (nalUnit.Header.NalUnitType == 39)
{
// Prefix SEI belongs to the following VCL NAL unit. Once this bounded item has started its only
// picture, another prefix unit would describe a second access unit that the item is not allowed to carry.
if (sliceSegments.Count != 0)
{
throw new InvalidImageContentException("The HEVC image item contains prefix SEI after its first coded slice.");
}
// Prefix SEI messages are associated with this item's only access unit. Parse the observable still-image
// state in NAL and message order without retaining generic video persistence or timing state.
supplementalEnhancementInformation.ReadPrefixNalUnit(nalUnit.Rbsp.Span);
}
}
if (sliceSegments.Count == 0)
{
throw new InvalidImageContentException("The HEVC image item contains no independently decodable picture.");
}
this.NalUnits = nalUnits;
this.SliceSegments = sliceSegments;
this.SupplementalEnhancementInformation = supplementalEnhancementInformation;
}
/// <summary>
/// Gets every decoded NAL unit in item order, including permitted delimiter, filler, and supplemental units.
/// </summary>
public IReadOnlyList<HevcNalUnit> NalUnits { get; }
/// <summary>
/// Gets the ordered slice segments that reconstruct the item's single IDR picture.
/// </summary>
public IReadOnlyList<HevcSliceSegmentHeader> SliceSegments { get; }
/// <summary>
/// Gets the presentation and exposed metadata decoded from prefix SEI NAL units.
/// </summary>
public HevcSupplementalEnhancementInformation SupplementalEnhancementInformation { get; }
/// <summary>
/// Reads an unsigned one-through-four-byte NAL-unit length without assuming four-byte item framing.
/// </summary>
/// <param name="data">The item bytes beginning at the length field.</param>
/// <param name="lengthSize">The codec-configuration-selected length-field width.</param>
/// <returns>The bounded signed integer NAL-unit length.</returns>
/// <exception cref="InvalidImageContentException">The unsigned length exceeds the supported item-buffer range.</exception>
private static int ReadNalUnitLength(ReadOnlySpan<byte> data, int lengthSize)
{
uint value = 0;
for (int byteIndex = 0; byteIndex < lengthSize; byteIndex++)
{
value = (value << 8) | data[byteIndex];
}
if (value > int.MaxValue)
{
throw new InvalidImageContentException("The HEVC image-item NAL-unit length is too large.");
}
return (int)value;
}
}

35
src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictionMode.cs

@ -1,35 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Provides the HEVC intra-mode values and chroma-format mapping shared by entropy decoding and reconstruction.
/// </summary>
internal static class HevcIntraPredictionMode
{
/// <summary>
/// The horizontal angular prediction mode.
/// </summary>
public const int Horizontal = 10;
/// <summary>
/// The vertical angular prediction mode.
/// </summary>
public const int Vertical = 26;
/// <summary>
/// Gets the 4:2:2 chroma intra-angle remapping defined by H.265 Table 8-4.
/// </summary>
private static ReadOnlySpan<byte> Chroma422AngleMap =>
[
0, 1, 2, 2, 2, 2, 3, 5, 7, 8, 10, 12, 13, 15, 17, 18, 19, 20, 21, 22, 23, 23, 24, 24, 25, 25, 26, 27, 27, 28, 28, 29, 29, 30, 31,
];
/// <summary>
/// Maps a coded chroma intra mode to the angular mode used by a 4:2:2 chroma block.
/// </summary>
/// <param name="mode">The effective coded chroma intra mode.</param>
/// <returns>The prediction angle used by the rectangular chroma block.</returns>
public static int RemapChroma422(int mode) => Chroma422AngleMap[mode];
}

394
src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictionState.cs

@ -1,394 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Stores and decodes the luma and chroma intra prediction modes for one HEVC still picture.
/// </summary>
internal sealed class HevcIntraPredictionState : IDisposable
{
/// <summary>
/// The base-two logarithm of the minimum luma prediction-block size.
/// </summary>
private const int MinPredictionBlockLog2 = 2;
/// <summary>
/// The planar intra prediction mode.
/// </summary>
private const byte PlanarMode = 0;
/// <summary>
/// The DC intra prediction mode.
/// </summary>
private const byte DcMode = 1;
/// <summary>
/// The horizontal intra prediction mode.
/// </summary>
private const byte HorizontalMode = 10;
/// <summary>
/// The vertical intra prediction mode.
/// </summary>
private const byte VerticalMode = 26;
/// <summary>
/// The replacement chroma mode used when an explicit chroma candidate equals the luma mode.
/// </summary>
private const byte ChromaReplacementMode = 34;
/// <summary>
/// The chroma mode that derives its direction from the colocated luma prediction block.
/// </summary>
private const byte DerivedChromaMode = 36;
/// <summary>
/// The luma intra mode at minimum-prediction-block resolution.
/// </summary>
private readonly Buffer2D<byte> lumaModes;
/// <summary>
/// The coded chroma intra mode at minimum-prediction-block resolution in luma coordinates.
/// </summary>
private readonly Buffer2D<byte> chromaModes;
/// <summary>
/// The resolved chroma intra mode at minimum-prediction-block resolution in luma coordinates.
/// </summary>
private readonly Buffer2D<byte> effectiveChromaModes;
/// <summary>
/// Whether derived chroma prediction selects the colocated luma prediction block.
/// </summary>
private readonly bool derivedChromaUsesColocatedLuma;
/// <summary>
/// The mask selecting a luma coordinate within its coding-tree block.
/// </summary>
private readonly int codingTreeBlockMask;
/// <summary>
/// Initializes a new instance of the <see cref="HevcIntraPredictionState"/> class.
/// </summary>
/// <param name="configuration">The configuration providing the image memory allocator.</param>
/// <param name="sequenceParameterSet">The coded luma picture dimensions.</param>
public HevcIntraPredictionState(Configuration configuration, HevcSequenceParameterSet sequenceParameterSet)
{
this.WidthInMinPredictionBlocks = DivideCeilingByPowerOfTwo(
sequenceParameterSet.Width,
MinPredictionBlockLog2);
this.HeightInMinPredictionBlocks = DivideCeilingByPowerOfTwo(
sequenceParameterSet.Height,
MinPredictionBlockLog2);
Buffer2D<byte>? lumaModes = null;
Buffer2D<byte>? chromaModes = null;
Buffer2D<byte>? effectiveChromaModes = null;
try
{
lumaModes = configuration.MemoryAllocator.Allocate2D<byte>(
this.WidthInMinPredictionBlocks,
this.HeightInMinPredictionBlocks);
chromaModes = configuration.MemoryAllocator.Allocate2D<byte>(
this.WidthInMinPredictionBlocks,
this.HeightInMinPredictionBlocks);
effectiveChromaModes = configuration.MemoryAllocator.Allocate2D<byte>(
this.WidthInMinPredictionBlocks,
this.HeightInMinPredictionBlocks);
// PCM coding units skip intra-mode syntax but remain available as most-probable-mode neighbors. HM
// initializes every luma direction to DC so those units provide the required default until syntax replaces it.
for (int row = 0; row < this.HeightInMinPredictionBlocks; row++)
{
lumaModes.DangerousGetRowSpan(row).Fill(DcMode);
}
this.lumaModes = lumaModes;
this.chromaModes = chromaModes;
this.effectiveChromaModes = effectiveChromaModes;
}
catch
{
effectiveChromaModes?.Dispose();
chromaModes?.Dispose();
lumaModes?.Dispose();
throw;
}
this.derivedChromaUsesColocatedLuma = sequenceParameterSet.ChromaFormat == 3;
this.codingTreeBlockMask = (1 << sequenceParameterSet.CodingTreeBlockLog2) - 1;
}
/// <summary>
/// Gets the map width in minimum luma prediction blocks.
/// </summary>
public int WidthInMinPredictionBlocks { get; }
/// <summary>
/// Gets the map height in minimum luma prediction blocks.
/// </summary>
public int HeightInMinPredictionBlocks { get; }
/// <summary>
/// Decodes and records the luma intra modes of one leaf coding unit.
/// </summary>
/// <param name="reader">The current entropy-substream syntax reader.</param>
/// <param name="x">The coding-unit left coordinate in luma samples.</param>
/// <param name="y">The coding-unit top coordinate in luma samples.</param>
/// <param name="log2Size">The base-two logarithm of the square coding-unit size.</param>
/// <param name="usesNxNPartitions">A value indicating whether the coding unit has four square prediction blocks.</param>
/// <param name="leftAvailable">A value indicating whether the external left prediction block is available.</param>
/// <param name="aboveAvailable">A value indicating whether the external above prediction block is available.</param>
public void DecodeLumaModes(
ref HevcCabacSyntaxReader reader,
int x,
int y,
int log2Size,
bool usesNxNPartitions,
bool leftAvailable,
bool aboveAvailable)
{
int predictionBlockLog2 = usesNxNPartitions ? log2Size - 1 : log2Size;
int predictionBlockSize = 1 << predictionBlockLog2;
int predictionBlockCount = usesNxNPartitions ? 4 : 1;
InlineArray4<byte> mostProbableFlags = default;
// HEVC codes every prev_intra_luma_pred_flag before any associated mode suffix. Preserve that two-pass
// ordering because decoding one complete mode at a time would consume a different CABAC bit sequence.
for (int index = 0; index < predictionBlockCount; index++)
{
mostProbableFlags[index] = reader.ReadPreviousIntraLumaPredictionFlag() ? (byte)1 : (byte)0;
}
InlineArray4<byte> mostProbableModes = default;
Span<byte> mostProbableModeSpan = mostProbableModes[..3];
for (int index = 0; index < predictionBlockCount; index++)
{
int offsetX = (index & 1) * predictionBlockSize;
int offsetY = (index >> 1) * predictionBlockSize;
int predictionX = x + offsetX;
int predictionY = y + offsetY;
bool predictionLeftAvailable = offsetX != 0 || leftAvailable;
// Luma MPM derivation treats an above prediction unit across a CTB boundary as unavailable. This is
// narrower than sample reconstruction availability and keeps the candidate order synchronized with CABAC.
bool predictionAboveAvailable = (predictionY & this.codingTreeBlockMask) != 0 && (offsetY != 0 || aboveAvailable);
this.GetMostProbableLumaModes(
predictionX,
predictionY,
predictionLeftAvailable,
predictionAboveAvailable,
mostProbableModeSpan);
int mode;
if (mostProbableFlags[index] != 0)
{
mode = mostProbableModeSpan[reader.ReadMostProbableIntraLumaPredictionIndex()];
}
else
{
SortThree(mostProbableModeSpan);
mode = reader.ReadRemainingIntraLumaPredictionMode();
for (int candidate = 0; candidate < mostProbableModeSpan.Length; candidate++)
{
// The remaining-mode code omits the three probable values, so each candidate at or below the
// provisional result advances the decoded mode over that omitted slot.
mode += mode >= mostProbableModeSpan[candidate] ? 1 : 0;
}
}
this.SetMode(this.lumaModes, predictionX, predictionY, predictionBlockLog2, (byte)mode);
}
}
/// <summary>
/// Decodes and records the chroma intra modes of one leaf coding unit.
/// </summary>
/// <param name="reader">The current entropy-substream syntax reader.</param>
/// <param name="x">The coding-unit left coordinate in luma samples.</param>
/// <param name="y">The coding-unit top coordinate in luma samples.</param>
/// <param name="log2Size">The base-two logarithm of the square coding-unit size.</param>
/// <param name="usesNxNPartitions">Whether the coding unit contains four luma prediction units.</param>
public void DecodeChromaModes(ref HevcCabacSyntaxReader reader, int x, int y, int log2Size, bool usesNxNPartitions)
{
bool usesFourChromaPredictionUnits = this.derivedChromaUsesColocatedLuma && usesNxNPartitions;
int predictionBlockLog2 = usesFourChromaPredictionUnits ? log2Size - 1 : log2Size;
int predictionBlockSize = 1 << predictionBlockLog2;
int predictionBlockCount = usesFourChromaPredictionUnits ? 4 : 1;
for (int index = 0; index < predictionBlockCount; index++)
{
int predictionX = x + ((index & 1) * predictionBlockSize);
int predictionY = y + ((index >> 1) * predictionBlockSize);
int selector = reader.ReadChromaPredictionModeIndex();
byte mode;
if (selector < 0)
{
mode = DerivedChromaMode;
}
else
{
ReadOnlySpan<byte> candidates = [PlanarMode, VerticalMode, HorizontalMode, DcMode];
mode = candidates[selector];
if (mode == this.GetLumaMode(predictionX, predictionY))
{
mode = ChromaReplacementMode;
}
}
// Combined 4:4:4 follows the four luma prediction partitions of an NxN coding unit. Subsampled formats
// carry one chroma mode for the coding unit and derive it from the top-left luma partition when requested.
this.SetMode(this.chromaModes, predictionX, predictionY, predictionBlockLog2, mode);
byte effectiveMode = mode == DerivedChromaMode ? this.GetLumaMode(predictionX, predictionY) : mode;
this.SetMode(this.effectiveChromaModes, predictionX, predictionY, predictionBlockLog2, effectiveMode);
}
}
/// <summary>
/// Gets the luma intra mode at a luma sample coordinate.
/// </summary>
/// <param name="x">The luma sample X coordinate.</param>
/// <param name="y">The luma sample Y coordinate.</param>
/// <returns>The luma intra mode in the inclusive range zero through thirty-four.</returns>
public byte GetLumaMode(int x, int y)
=> this.lumaModes.DangerousGetRowSpan(y >> MinPredictionBlockLog2)[x >> MinPredictionBlockLog2];
/// <summary>
/// Gets the coded chroma intra mode at a luma sample coordinate.
/// </summary>
/// <param name="x">The luma sample X coordinate.</param>
/// <param name="y">The luma sample Y coordinate.</param>
/// <returns>An explicit chroma direction or the derived-mode value.</returns>
public byte GetChromaMode(int x, int y)
=> this.chromaModes.DangerousGetRowSpan(y >> MinPredictionBlockLog2)[x >> MinPredictionBlockLog2];
/// <summary>
/// Gets the effective chroma intra mode at a luma sample coordinate.
/// </summary>
/// <param name="x">The luma sample X coordinate.</param>
/// <param name="y">The luma sample Y coordinate.</param>
/// <returns>The explicit chroma mode, or the colocated luma mode when chroma uses derived mode.</returns>
public byte GetEffectiveChromaMode(int x, int y)
=> this.effectiveChromaModes.DangerousGetRowSpan(y >> MinPredictionBlockLog2)[x >> MinPredictionBlockLog2];
/// <summary>
/// Releases the owned intra-mode maps.
/// </summary>
public void Dispose()
{
this.lumaModes.Dispose();
this.chromaModes.Dispose();
this.effectiveChromaModes.Dispose();
}
/// <summary>
/// Derives the three most-probable luma intra modes from available spatial neighbors.
/// </summary>
/// <param name="x">The prediction-block left coordinate in luma samples.</param>
/// <param name="y">The prediction-block top coordinate in luma samples.</param>
/// <param name="leftAvailable">A value indicating whether the left prediction block is available.</param>
/// <param name="aboveAvailable">A value indicating whether the above prediction block is available.</param>
/// <param name="modes">The three-element destination span.</param>
private void GetMostProbableLumaModes(
int x,
int y,
bool leftAvailable,
bool aboveAvailable,
Span<byte> modes)
{
byte leftMode = leftAvailable ? this.GetLumaMode(x - 1, y) : DcMode;
byte aboveMode = aboveAvailable ? this.GetLumaMode(x, y - 1) : DcMode;
if (leftMode == aboveMode)
{
if (leftMode > DcMode)
{
modes[0] = leftMode;
modes[1] = (byte)(((leftMode + 29) % 32) + 2);
modes[2] = (byte)(((leftMode - 1) % 32) + 2);
}
else
{
modes[0] = PlanarMode;
modes[1] = DcMode;
modes[2] = VerticalMode;
}
return;
}
modes[0] = leftMode;
modes[1] = aboveMode;
if (leftMode != PlanarMode && aboveMode != PlanarMode)
{
modes[2] = PlanarMode;
}
else
{
modes[2] = leftMode + aboveMode < 2 ? VerticalMode : DcMode;
}
}
/// <summary>
/// Records one prediction mode over a square luma-coordinate region.
/// </summary>
/// <param name="map">The luma or chroma mode map.</param>
/// <param name="x">The region left coordinate in luma samples.</param>
/// <param name="y">The region top coordinate in luma samples.</param>
/// <param name="log2Size">The base-two logarithm of the square region size.</param>
/// <param name="mode">The prediction mode.</param>
private void SetMode(Buffer2D<byte> map, int x, int y, int log2Size, byte mode)
{
int unitX = x >> MinPredictionBlockLog2;
int unitY = y >> MinPredictionBlockLog2;
int unitCount = 1 << (log2Size - MinPredictionBlockLog2);
int endX = Math.Min(unitX + unitCount, this.WidthInMinPredictionBlocks);
int endY = Math.Min(unitY + unitCount, this.HeightInMinPredictionBlocks);
for (int row = unitY; row < endY; row++)
{
map.DangerousGetRowSpan(row)[unitX..endX].Fill(mode);
}
}
/// <summary>
/// Sorts three intra-mode values into ascending order.
/// </summary>
/// <param name="values">The three-element mode span.</param>
private static void SortThree(Span<byte> values)
{
if (values[0] > values[1])
{
byte value = values[0];
values[0] = values[1];
values[1] = value;
}
if (values[0] > values[2])
{
byte value = values[0];
values[0] = values[2];
values[2] = value;
}
if (values[1] > values[2])
{
byte value = values[1];
values[1] = values[2];
values[2] = value;
}
}
/// <summary>
/// Divides a nonnegative sample count by a power of two with upward rounding.
/// </summary>
/// <param name="value">The sample count.</param>
/// <param name="shift">The base-two divisor logarithm.</param>
/// <returns>The upward-rounded quotient.</returns>
private static int DivideCeilingByPowerOfTwo(int value, int shift) => (value + (1 << shift) - 1) >> shift;
}

346
src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictor.AngularOperator.cs

@ -1,346 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Defines angular intra-prediction arithmetic.
/// </content>
internal static partial class HevcIntraPredictor
{
/// <summary>
/// Implements the thirty-three directional intra-prediction modes.
/// </summary>
private readonly struct AngularOperator : IHevcIntraPredictionOperator
{
/// <inheritdoc/>
public static void Predict(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int size,
int mode,
int bitDepth,
bool filterPredictionEdges,
Span<ushort> scratch)
{
if (mode == VerticalMode)
{
PredictVertical(top, left, destination, destinationStride, size, bitDepth, filterPredictionEdges);
return;
}
if (mode == HorizontalMode)
{
PredictHorizontal(top, left, destination, destinationStride, size, bitDepth, filterPredictionEdges);
return;
}
bool vertical = mode >= FirstVerticalMode;
int angleMode = vertical ? mode - VerticalMode : HorizontalMode - mode;
int absoluteAngleMode = Math.Abs(angleMode);
int angle = PredictionAngles[absoluteAngleMode] * Math.Sign(angleMode);
ReadOnlySpan<ushort> main = vertical ? top : left;
ReadOnlySpan<ushort> side = vertical ? left : top;
Span<ushort> temporaryBlock = scratch[..(size * size)];
Span<ushort> extendedReference = scratch.Slice(size * size, (4 * size) + 1);
int mainOrigin = 0;
if (angle < 0)
{
mainOrigin = size * 2;
main[..(size + 1)].CopyTo(extendedReference[mainOrigin..]);
int inverseAngle = InversePredictionAngles[absoluteAngleMode];
int inverseAngleSum = 128;
int minimumIndex = (size * angle) >> 5;
for (int index = -1; index > minimumIndex; index--)
{
inverseAngleSum += inverseAngle;
extendedReference[mainOrigin + index] = side[inverseAngleSum >> 8];
}
main = extendedReference;
}
Span<ushort> prediction = vertical ? destination : temporaryBlock;
int predictionStride = vertical ? destinationStride : size;
PredictAngularRows(main, mainOrigin, prediction, predictionStride, size, angle);
if (!vertical)
{
TransposeBlock(temporaryBlock, destination, destinationStride, size);
}
}
/// <summary>
/// Copies the top reference into every row and optionally filters the first column.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference samples.</param>
/// <param name="destination">The destination block origin.</param>
/// <param name="destinationStride">The destination row stride.</param>
/// <param name="size">The square block side.</param>
/// <param name="bitDepth">The reconstructed component precision.</param>
/// <param name="filterPredictionEdges">Whether the vertical luma edge filter applies.</param>
private static void PredictVertical(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int size,
int bitDepth,
bool filterPredictionEdges)
{
ReadOnlySpan<ushort> row = top.Slice(1, size);
int maximum = (1 << bitDepth) - 1;
for (int y = 0; y < size; y++)
{
row.CopyTo(destination.Slice(y * destinationStride, size));
if (filterPredictionEdges)
{
int sample = destination[y * destinationStride] + ((left[y + 1] - left[0]) >> 1);
destination[y * destinationStride] = (ushort)Math.Clamp(sample, 0, maximum);
}
}
}
/// <summary>
/// Fills each row from its left reference and optionally filters the first row.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="left">The left reference samples.</param>
/// <param name="destination">The destination block origin.</param>
/// <param name="destinationStride">The destination row stride.</param>
/// <param name="size">The square block side.</param>
/// <param name="bitDepth">The reconstructed component precision.</param>
/// <param name="filterPredictionEdges">Whether the horizontal luma edge filter applies.</param>
private static void PredictHorizontal(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int size,
int bitDepth,
bool filterPredictionEdges)
{
for (int y = 0; y < size; y++)
{
destination.Slice(y * destinationStride, size).Fill(left[y + 1]);
}
if (!filterPredictionEdges)
{
return;
}
int maximum = (1 << bitDepth) - 1;
for (int x = 0; x < size; x++)
{
int sample = destination[x] + ((top[x + 1] - top[0]) >> 1);
destination[x] = (ushort)Math.Clamp(sample, 0, maximum);
}
}
/// <summary>
/// Generates a vertical-oriented angular block using contiguous SIMD interpolation within each row.
/// </summary>
/// <param name="main">The main reference beginning at logical index zero.</param>
/// <param name="mainOrigin">The span index corresponding to logical reference index zero.</param>
/// <param name="destination">The contiguous destination or transposition scratch block.</param>
/// <param name="destinationStride">The destination row stride.</param>
/// <param name="size">The square block side.</param>
/// <param name="angle">The signed prediction displacement in thirty-second-sample units.</param>
private static void PredictAngularRows(
ReadOnlySpan<ushort> main,
int mainOrigin,
Span<ushort> destination,
int destinationStride,
int size,
int angle)
{
for (int y = 0, deltaPosition = angle; y < size; y++, deltaPosition += angle)
{
int deltaInteger = deltaPosition >> 5;
int deltaFraction = deltaPosition & 31;
int sourceOffset = mainOrigin + deltaInteger + 1;
Span<ushort> row = destination.Slice(y * destinationStride, size);
if (deltaFraction == 0)
{
main.Slice(sourceOffset, size).CopyTo(row);
}
else
{
InterpolateAngularRow(main[sourceOffset..], row, deltaFraction);
}
}
}
/// <summary>
/// Interpolates one angular prediction row between consecutive main-reference samples.
/// </summary>
/// <param name="source">The first main-reference sample for the row.</param>
/// <param name="destination">The destination prediction row.</param>
/// <param name="fraction">The right-hand weight with a denominator of thirty-two.</param>
private static void InterpolateAngularRow(ReadOnlySpan<ushort> source, Span<ushort> destination, int fraction)
{
ref ushort sourceBase = ref MemoryMarshal.GetReference(source);
ref ushort destinationBase = ref MemoryMarshal.GetReference(destination);
uint leftWeight = (uint)(32 - fraction);
uint rightWeight = (uint)fraction;
int i = 0;
// Adjacent source vectors overlap by one sample, aligning each left/right reference pair in the same lane.
// Widening keeps the largest 12-bit Q5 weighted sum below the UInt32 limit before narrowing to sample storage.
if (Vector512.IsHardwareAccelerated)
{
int oneVectorFromEnd = destination.Length - Vector512<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<ushort>.Count)
{
Vector512<ushort> left = Vector512.LoadUnsafe(ref sourceBase, (nuint)i);
Vector512<ushort> right = Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + 1));
(Vector512<uint> leftLow, Vector512<uint> leftHigh) = Vector512.Widen(left);
(Vector512<uint> rightLow, Vector512<uint> rightHigh) = Vector512.Widen(right);
Vector512<uint> low = ((leftLow * leftWeight) + (rightLow * rightWeight) + Vector512.Create(16U)) >> 5;
Vector512<uint> high = ((leftHigh * leftWeight) + (rightHigh * rightWeight) + Vector512.Create(16U)) >> 5;
Vector512.Narrow(low, high).StoreUnsafe(ref Unsafe.Add(ref destinationBase, i));
}
}
if (Vector256.IsHardwareAccelerated)
{
int oneVectorFromEnd = destination.Length - Vector256<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<ushort>.Count)
{
Vector256<ushort> left = Vector256.LoadUnsafe(ref sourceBase, (nuint)i);
Vector256<ushort> right = Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + 1));
(Vector256<uint> leftLow, Vector256<uint> leftHigh) = Vector256.Widen(left);
(Vector256<uint> rightLow, Vector256<uint> rightHigh) = Vector256.Widen(right);
Vector256<uint> low = ((leftLow * leftWeight) + (rightLow * rightWeight) + Vector256.Create(16U)) >> 5;
Vector256<uint> high = ((leftHigh * leftWeight) + (rightHigh * rightWeight) + Vector256.Create(16U)) >> 5;
Vector256.Narrow(low, high).StoreUnsafe(ref Unsafe.Add(ref destinationBase, i));
}
}
if (Vector128.IsHardwareAccelerated)
{
int oneVectorFromEnd = destination.Length - Vector128<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<ushort>.Count)
{
Vector128<ushort> left = Vector128.LoadUnsafe(ref sourceBase, (nuint)i);
Vector128<ushort> right = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + 1));
(Vector128<uint> leftLow, Vector128<uint> leftHigh) = Vector128.Widen(left);
(Vector128<uint> rightLow, Vector128<uint> rightHigh) = Vector128.Widen(right);
Vector128<uint> low = ((leftLow * leftWeight) + (rightLow * rightWeight) + Vector128.Create(16U)) >> 5;
Vector128<uint> high = ((leftHigh * leftWeight) + (rightHigh * rightWeight) + Vector128.Create(16U)) >> 5;
Vector128.Narrow(low, high).StoreUnsafe(ref Unsafe.Add(ref destinationBase, i));
}
}
for (; i < destination.Length; i++)
{
Unsafe.Add(ref destinationBase, i) = (ushort)(((source[i] * leftWeight) + (source[i + 1] * rightWeight) + 16) >> 5);
}
}
/// <summary>
/// Transposes a square horizontal prediction block into the reconstructed destination.
/// </summary>
/// <param name="source">The contiguous transposed prediction block.</param>
/// <param name="destination">The destination block origin.</param>
/// <param name="destinationStride">The destination row stride.</param>
/// <param name="size">The square block side.</param>
private static void TransposeBlock(ReadOnlySpan<ushort> source, Span<ushort> destination, int destinationStride, int size)
{
if (Vector128.IsHardwareAccelerated && size >= Vector128<ushort>.Count)
{
for (int y = 0; y < size; y += Vector128<ushort>.Count)
{
for (int x = 0; x < size; x += Vector128<ushort>.Count)
{
Transpose8x8(source, destination, destinationStride, size, x, y);
}
}
return;
}
for (int y = 0; y < size; y++)
{
for (int x = 0; x < size; x++)
{
destination[(x * destinationStride) + y] = source[(y * size) + x];
}
}
}
/// <summary>
/// Transposes one eight-by-eight tile of 16-bit prediction samples.
/// </summary>
/// <param name="source">The contiguous source block.</param>
/// <param name="destination">The destination block origin.</param>
/// <param name="destinationStride">The destination row stride.</param>
/// <param name="sourceStride">The contiguous source row stride.</param>
/// <param name="x">The tile X coordinate in the source block.</param>
/// <param name="y">The tile Y coordinate in the source block.</param>
private static void Transpose8x8(
ReadOnlySpan<ushort> source,
Span<ushort> destination,
int destinationStride,
int sourceStride,
int x,
int y)
{
ref ushort sourceBase = ref MemoryMarshal.GetReference(source);
ref ushort destinationBase = ref MemoryMarshal.GetReference(destination);
Vector128<short> row0 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 0) * sourceStride) + x)).AsInt16();
Vector128<short> row1 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 1) * sourceStride) + x)).AsInt16();
Vector128<short> row2 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 2) * sourceStride) + x)).AsInt16();
Vector128<short> row3 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 3) * sourceStride) + x)).AsInt16();
Vector128<short> row4 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 4) * sourceStride) + x)).AsInt16();
Vector128<short> row5 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 5) * sourceStride) + x)).AsInt16();
Vector128<short> row6 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 6) * sourceStride) + x)).AsInt16();
Vector128<short> row7 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 7) * sourceStride) + x)).AsInt16();
// Three zip stages exchange one, two, then four 16-bit coordinates. The resulting vectors are the eight
// source columns in row order, so each can be stored contiguously into one destination row.
Vector128<short> pair0 = Vector128_.UnpackLow(row0, row1);
Vector128<short> pair1 = Vector128_.UnpackHigh(row0, row1);
Vector128<short> pair2 = Vector128_.UnpackLow(row2, row3);
Vector128<short> pair3 = Vector128_.UnpackHigh(row2, row3);
Vector128<short> pair4 = Vector128_.UnpackLow(row4, row5);
Vector128<short> pair5 = Vector128_.UnpackHigh(row4, row5);
Vector128<short> pair6 = Vector128_.UnpackLow(row6, row7);
Vector128<short> pair7 = Vector128_.UnpackHigh(row6, row7);
Vector128<int> quad0 = Vector128_.UnpackLow(pair0.AsInt32(), pair2.AsInt32());
Vector128<int> quad1 = Vector128_.UnpackHigh(pair0.AsInt32(), pair2.AsInt32());
Vector128<int> quad2 = Vector128_.UnpackLow(pair1.AsInt32(), pair3.AsInt32());
Vector128<int> quad3 = Vector128_.UnpackHigh(pair1.AsInt32(), pair3.AsInt32());
Vector128<int> quad4 = Vector128_.UnpackLow(pair4.AsInt32(), pair6.AsInt32());
Vector128<int> quad5 = Vector128_.UnpackHigh(pair4.AsInt32(), pair6.AsInt32());
Vector128<int> quad6 = Vector128_.UnpackLow(pair5.AsInt32(), pair7.AsInt32());
Vector128<int> quad7 = Vector128_.UnpackHigh(pair5.AsInt32(), pair7.AsInt32());
Vector128<ushort> column0 = Vector128_.UnpackLow(quad0.AsInt64(), quad4.AsInt64()).AsUInt16();
Vector128<ushort> column1 = Vector128_.UnpackHigh(quad0.AsInt64(), quad4.AsInt64()).AsUInt16();
Vector128<ushort> column2 = Vector128_.UnpackLow(quad1.AsInt64(), quad5.AsInt64()).AsUInt16();
Vector128<ushort> column3 = Vector128_.UnpackHigh(quad1.AsInt64(), quad5.AsInt64()).AsUInt16();
Vector128<ushort> column4 = Vector128_.UnpackLow(quad2.AsInt64(), quad6.AsInt64()).AsUInt16();
Vector128<ushort> column5 = Vector128_.UnpackHigh(quad2.AsInt64(), quad6.AsInt64()).AsUInt16();
Vector128<ushort> column6 = Vector128_.UnpackLow(quad3.AsInt64(), quad7.AsInt64()).AsUInt16();
Vector128<ushort> column7 = Vector128_.UnpackHigh(quad3.AsInt64(), quad7.AsInt64()).AsUInt16();
column0.StoreUnsafe(ref destinationBase, (nuint)(((x + 0) * destinationStride) + y));
column1.StoreUnsafe(ref destinationBase, (nuint)(((x + 1) * destinationStride) + y));
column2.StoreUnsafe(ref destinationBase, (nuint)(((x + 2) * destinationStride) + y));
column3.StoreUnsafe(ref destinationBase, (nuint)(((x + 3) * destinationStride) + y));
column4.StoreUnsafe(ref destinationBase, (nuint)(((x + 4) * destinationStride) + y));
column5.StoreUnsafe(ref destinationBase, (nuint)(((x + 5) * destinationStride) + y));
column6.StoreUnsafe(ref destinationBase, (nuint)(((x + 6) * destinationStride) + y));
column7.StoreUnsafe(ref destinationBase, (nuint)(((x + 7) * destinationStride) + y));
}
}
}

108
src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictor.DcOperator.cs

@ -1,108 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Defines DC intra-prediction arithmetic.
/// </content>
internal static partial class HevcIntraPredictor
{
/// <summary>
/// Implements DC prediction and its optional luma boundary filter.
/// </summary>
private readonly struct DcOperator : IHevcIntraPredictionOperator
{
/// <inheritdoc/>
public static void Predict(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int size,
int mode,
int bitDepth,
bool filterPredictionEdges,
Span<ushort> scratch)
{
uint sum = SumSamples(top.Slice(1, size)) + SumSamples(left.Slice(1, size));
ushort dc = (ushort)((sum + (uint)size) >> (BitOperations.Log2((uint)size) + 1));
for (int y = 0; y < size; y++)
{
destination.Slice(y * destinationStride, size).Fill(dc);
}
if (!filterPredictionEdges)
{
return;
}
destination[0] = (ushort)((top[1] + left[1] + (2 * dc) + 2) >> 2);
for (int x = 1; x < size; x++)
{
destination[x] = (ushort)((top[x + 1] + (3 * dc) + 2) >> 2);
}
for (int y = 1; y < size; y++)
{
destination[y * destinationStride] = (ushort)((left[y + 1] + (3 * dc) + 2) >> 2);
}
}
/// <summary>
/// Sums reconstructed reference samples without overflowing their 16-bit storage.
/// </summary>
/// <param name="samples">The samples to sum.</param>
/// <returns>The exact unsigned sum.</returns>
private static uint SumSamples(ReadOnlySpan<ushort> samples)
{
ref ushort samplesBase = ref MemoryMarshal.GetReference(samples);
uint sum = 0;
int i = 0;
// Widen before reduction because a complete 64-sample, 12-bit reference edge exceeds UInt16. The shared index
// lets narrower vectors consume only the remainder from the widest available path.
if (Vector512.IsHardwareAccelerated)
{
int oneVectorFromEnd = samples.Length - Vector512<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<ushort>.Count)
{
(Vector512<uint> low, Vector512<uint> high) = Vector512.Widen(Vector512.LoadUnsafe(ref samplesBase, (nuint)i));
sum += Vector512.Sum(low) + Vector512.Sum(high);
}
}
if (Vector256.IsHardwareAccelerated)
{
int oneVectorFromEnd = samples.Length - Vector256<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<ushort>.Count)
{
(Vector256<uint> low, Vector256<uint> high) = Vector256.Widen(Vector256.LoadUnsafe(ref samplesBase, (nuint)i));
sum += Vector256.Sum(low) + Vector256.Sum(high);
}
}
if (Vector128.IsHardwareAccelerated)
{
int oneVectorFromEnd = samples.Length - Vector128<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<ushort>.Count)
{
(Vector128<uint> low, Vector128<uint> high) = Vector128.Widen(Vector128.LoadUnsafe(ref samplesBase, (nuint)i));
sum += Vector128.Sum(low) + Vector128.Sum(high);
}
}
for (; i < samples.Length; i++)
{
sum += Unsafe.Add(ref samplesBase, i);
}
return sum;
}
}
}

39
src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictor.Operator.cs

@ -1,39 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Defines the HEVC intra-prediction operator contract.
/// </content>
internal static partial class HevcIntraPredictor
{
/// <summary>
/// Defines one closed intra-prediction operation selected by the decoded mode.
/// </summary>
private interface IHevcIntraPredictionOperator
{
/// <summary>
/// Reconstructs one square prediction block.
/// </summary>
/// <param name="top">The top-left, top, and top-right reference samples.</param>
/// <param name="left">The top-left, left, and below-left reference samples.</param>
/// <param name="destination">The destination buffer beginning at the block origin.</param>
/// <param name="destinationStride">The destination row stride in samples.</param>
/// <param name="size">The square block side in samples.</param>
/// <param name="mode">The decoded prediction mode.</param>
/// <param name="bitDepth">The reconstructed component precision.</param>
/// <param name="filterPredictionEdges">Whether the luma edge filter applies to the selected block.</param>
/// <param name="scratch">The caller-owned block and extended-reference scratch space.</param>
public static abstract void Predict(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int size,
int mode,
int bitDepth,
bool filterPredictionEdges,
Span<ushort> scratch);
}
}

271
src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictor.PlanarOperator.cs

@ -1,271 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Defines planar intra-prediction arithmetic.
/// </content>
internal static partial class HevcIntraPredictor
{
/// <summary>
/// Implements planar interpolation between the top, left, bottom-left, and top-right references.
/// </summary>
private readonly struct PlanarOperator : IHevcIntraPredictionOperator
{
/// <inheritdoc/>
public static void Predict(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int size,
int mode,
int bitDepth,
bool filterPredictionEdges,
Span<ushort> scratch)
{
ref ushort topBase = ref MemoryMarshal.GetReference(top);
ref ushort destinationBase = ref MemoryMarshal.GetReference(destination);
// Index zero is the shared corner, so the planar endpoint at coordinate N is stored at N + 1.
uint bottomLeft = left[size + 1];
uint topRight = top[size + 1];
int shift = BitOperations.Log2((uint)size) + 1;
uint rounding = (uint)size;
for (int y = 0; y < size; y++)
{
uint leftSample = left[y + 1];
uint topWeight = (uint)(size - y - 1);
uint bottomWeight = (uint)(y + 1);
ref ushort rowBase = ref Unsafe.Add(ref destinationBase, y * destinationStride);
int x = 0;
// The two widened halves carry consecutive X coordinates. Each lane evaluates the normative
// horizontal and vertical ramps, then narrows after the common rounded power-of-two division.
if (Vector512.IsHardwareAccelerated)
{
Vector512<uint> indices = CreateIndicesVector512();
int oneVectorFromEnd = size - Vector512<ushort>.Count;
for (; x <= oneVectorFromEnd; x += Vector512<ushort>.Count)
{
Vector512<ushort> topSamples = Vector512.LoadUnsafe(ref topBase, (nuint)(x + 1));
(Vector512<uint> topLow, Vector512<uint> topHigh) = Vector512.Widen(topSamples);
Vector512<uint> lowIndices = indices + Vector512.Create((uint)x);
Vector512<uint> highIndices = lowIndices + Vector512.Create((uint)Vector512<uint>.Count);
Vector512<uint> low = CalculatePlanarVector(
topLow,
lowIndices,
leftSample,
topRight,
bottomLeft,
topWeight,
bottomWeight,
(uint)size,
rounding,
shift);
Vector512<uint> high = CalculatePlanarVector(
topHigh,
highIndices,
leftSample,
topRight,
bottomLeft,
topWeight,
bottomWeight,
(uint)size,
rounding,
shift);
Vector512.Narrow(low, high).StoreUnsafe(ref Unsafe.Add(ref rowBase, x));
}
}
if (Vector256.IsHardwareAccelerated)
{
Vector256<uint> indices = CreateIndicesVector256();
int oneVectorFromEnd = size - Vector256<ushort>.Count;
for (; x <= oneVectorFromEnd; x += Vector256<ushort>.Count)
{
Vector256<ushort> topSamples = Vector256.LoadUnsafe(ref topBase, (nuint)(x + 1));
(Vector256<uint> topLow, Vector256<uint> topHigh) = Vector256.Widen(topSamples);
Vector256<uint> lowIndices = indices + Vector256.Create((uint)x);
Vector256<uint> highIndices = lowIndices + Vector256.Create((uint)Vector256<uint>.Count);
Vector256<uint> low = CalculatePlanarVector(
topLow,
lowIndices,
leftSample,
topRight,
bottomLeft,
topWeight,
bottomWeight,
(uint)size,
rounding,
shift);
Vector256<uint> high = CalculatePlanarVector(
topHigh,
highIndices,
leftSample,
topRight,
bottomLeft,
topWeight,
bottomWeight,
(uint)size,
rounding,
shift);
Vector256.Narrow(low, high).StoreUnsafe(ref Unsafe.Add(ref rowBase, x));
}
}
if (Vector128.IsHardwareAccelerated)
{
Vector128<uint> indices = CreateIndicesVector128();
int oneVectorFromEnd = size - Vector128<ushort>.Count;
for (; x <= oneVectorFromEnd; x += Vector128<ushort>.Count)
{
Vector128<ushort> topSamples = Vector128.LoadUnsafe(ref topBase, (nuint)(x + 1));
(Vector128<uint> topLow, Vector128<uint> topHigh) = Vector128.Widen(topSamples);
Vector128<uint> lowIndices = indices + Vector128.Create((uint)x);
Vector128<uint> highIndices = lowIndices + Vector128.Create((uint)Vector128<uint>.Count);
Vector128<uint> low = CalculatePlanarVector(
topLow,
lowIndices,
leftSample,
topRight,
bottomLeft,
topWeight,
bottomWeight,
(uint)size,
rounding,
shift);
Vector128<uint> high = CalculatePlanarVector(
topHigh,
highIndices,
leftSample,
topRight,
bottomLeft,
topWeight,
bottomWeight,
(uint)size,
rounding,
shift);
Vector128.Narrow(low, high).StoreUnsafe(ref Unsafe.Add(ref rowBase, x));
}
}
for (; x < size; x++)
{
uint horizontal = ((uint)(size - x - 1) * leftSample) + ((uint)(x + 1) * topRight);
uint vertical = ((uint)(size - y - 1) * top[x + 1]) + ((uint)(y + 1) * bottomLeft);
Unsafe.Add(ref rowBase, x) = (ushort)((horizontal + vertical + (uint)size) >> shift);
}
}
}
/// <summary>
/// Calculates one 512-bit half of a planar prediction row.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="indices">The zero-based X coordinates.</param>
/// <param name="left">The left reference sample for the row.</param>
/// <param name="topRight">The top-right reference sample.</param>
/// <param name="bottomLeft">The bottom-left reference sample.</param>
/// <param name="topWeight">The top-reference weight.</param>
/// <param name="bottomWeight">The bottom-left-reference weight.</param>
/// <param name="size">The square block side.</param>
/// <param name="rounding">The division rounding constant.</param>
/// <param name="shift">The division shift.</param>
/// <returns>The predicted samples as widened lanes.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<uint> CalculatePlanarVector(
Vector512<uint> top,
Vector512<uint> indices,
uint left,
uint topRight,
uint bottomLeft,
uint topWeight,
uint bottomWeight,
uint size,
uint rounding,
int shift)
{
Vector512<uint> horizontal = ((Vector512.Create(size - 1) - indices) * left) + ((indices + Vector512<uint>.One) * topRight);
Vector512<uint> vertical = (top * topWeight) + Vector512.Create(bottomLeft * bottomWeight);
return (horizontal + vertical + Vector512.Create(rounding)) >> shift;
}
/// <summary>
/// Calculates one 256-bit half of a planar prediction row.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="indices">The zero-based X coordinates.</param>
/// <param name="left">The left reference sample for the row.</param>
/// <param name="topRight">The top-right reference sample.</param>
/// <param name="bottomLeft">The bottom-left reference sample.</param>
/// <param name="topWeight">The top-reference weight.</param>
/// <param name="bottomWeight">The bottom-left-reference weight.</param>
/// <param name="size">The square block side.</param>
/// <param name="rounding">The division rounding constant.</param>
/// <param name="shift">The division shift.</param>
/// <returns>The predicted samples as widened lanes.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<uint> CalculatePlanarVector(
Vector256<uint> top,
Vector256<uint> indices,
uint left,
uint topRight,
uint bottomLeft,
uint topWeight,
uint bottomWeight,
uint size,
uint rounding,
int shift)
{
Vector256<uint> horizontal = ((Vector256.Create(size - 1) - indices) * left) + ((indices + Vector256<uint>.One) * topRight);
Vector256<uint> vertical = (top * topWeight) + Vector256.Create(bottomLeft * bottomWeight);
return (horizontal + vertical + Vector256.Create(rounding)) >> shift;
}
/// <summary>
/// Calculates one 128-bit half of a planar prediction row.
/// </summary>
/// <param name="top">The top reference samples.</param>
/// <param name="indices">The zero-based X coordinates.</param>
/// <param name="left">The left reference sample for the row.</param>
/// <param name="topRight">The top-right reference sample.</param>
/// <param name="bottomLeft">The bottom-left reference sample.</param>
/// <param name="topWeight">The top-reference weight.</param>
/// <param name="bottomWeight">The bottom-left-reference weight.</param>
/// <param name="size">The square block side.</param>
/// <param name="rounding">The division rounding constant.</param>
/// <param name="shift">The division shift.</param>
/// <returns>The predicted samples as widened lanes.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<uint> CalculatePlanarVector(
Vector128<uint> top,
Vector128<uint> indices,
uint left,
uint topRight,
uint bottomLeft,
uint topWeight,
uint bottomWeight,
uint size,
uint rounding,
int shift)
{
Vector128<uint> horizontal = ((Vector128.Create(size - 1) - indices) * left) + ((indices + Vector128<uint>.One) * topRight);
Vector128<uint> vertical = (top * topWeight) + Vector128.Create(bottomLeft * bottomWeight);
return (horizontal + vertical + Vector128.Create(rounding)) >> shift;
}
}
}

180
src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictor.References.cs

@ -1,180 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Provides reference-sample preparation and filter selection.
/// </content>
internal static partial class HevcIntraPredictor
{
/// <summary>
/// Gets the angular-distance threshold for reference filtering at each supported block size.
/// </summary>
private static ReadOnlySpan<byte> ReferenceFilterThresholds => [10, 7, 1, 0];
/// <summary>
/// Gets the temporary sample count required while preparing prediction references.
/// </summary>
/// <param name="log2Size">The base-two logarithm of the square prediction-block side.</param>
/// <param name="unitWidth">The horizontal availability-unit width in plane samples.</param>
/// <returns>The required number of <see cref="ushort"/> elements.</returns>
public static int GetReferenceScratchLength(int log2Size, int unitWidth)
{
DebugGuard.MustBeBetweenOrEqualTo(log2Size, 2, 5, nameof(log2Size));
return (4 << log2Size) + unitWidth;
}
/// <summary>
/// Determines whether the selected mode uses filtered prediction references.
/// </summary>
/// <param name="plane">The reconstructed plane.</param>
/// <param name="mode">The effective prediction mode in the inclusive range zero through thirty-four.</param>
/// <param name="log2Size">The base-two logarithm of the square prediction-block side.</param>
/// <param name="chromaFormat">The HEVC chroma-format identifier.</param>
/// <param name="intraSmoothingDisabled">Whether the sequence disables intra-reference smoothing.</param>
/// <returns><see langword="true"/> when the prepared references require filtering; otherwise, <see langword="false"/>.</returns>
public static bool ShouldFilterReferenceSamples(HevcPlane plane, int mode, int log2Size, byte chromaFormat, bool intraSmoothingDisabled)
{
DebugGuard.MustBeBetweenOrEqualTo(mode, PlanarMode, 34, nameof(mode));
DebugGuard.MustBeBetweenOrEqualTo(log2Size, 2, 5, nameof(log2Size));
if (intraSmoothingDisabled || (plane != HevcPlane.Y && chromaFormat != 3) || mode == DcMode)
{
return false;
}
int angularDistance = Math.Min(Math.Abs(mode - HorizontalMode), Math.Abs(mode - VerticalMode));
return angularDistance > ReferenceFilterThresholds[log2Size - 2];
}
/// <summary>
/// Prepares substituted top and left references from one reconstructed picture plane.
/// </summary>
/// <param name="picture">The reconstructed still-picture planes.</param>
/// <param name="plane">The plane containing the prediction block.</param>
/// <param name="x">The prediction-block left coordinate in plane samples.</param>
/// <param name="y">The prediction-block top coordinate in plane samples.</param>
/// <param name="log2Size">The base-two logarithm of the square prediction-block side.</param>
/// <param name="unitWidth">The horizontal availability-unit width in plane samples.</param>
/// <param name="unitHeight">The vertical availability-unit height in plane samples.</param>
/// <param name="availableUnits">
/// The availability flags ordered from the bottom-most below-left unit upward through top-left, then from the
/// left-most above unit through the right-most above-right unit.
/// </param>
/// <param name="top">The destination top-left, top, and top-right reference samples.</param>
/// <param name="left">The destination top-left, left, and below-left reference samples.</param>
/// <param name="scratch">The caller-owned temporary storage sized by <see cref="GetReferenceScratchLength(int, int)"/>.</param>
public static void PrepareReferenceSamples(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int log2Size,
int unitWidth,
int unitHeight,
ReadOnlySpan<bool> availableUnits,
Span<ushort> top,
Span<ushort> left,
Span<ushort> scratch)
{
DebugGuard.MustBeBetweenOrEqualTo(log2Size, 2, 5, nameof(log2Size));
int size = 1 << log2Size;
int referenceLength = (size * 2) + 1;
int leftUnitCount = (size * 2) / unitHeight;
int aboveUnitCount = (size * 2) / unitWidth;
int totalUnitCount = leftUnitCount + aboveUnitCount + 1;
ReadOnlySpan<bool> availability = availableUnits[..totalUnitCount];
if (availability.IndexOf(true) < 0)
{
ushort midpoint = (ushort)(1 << (picture.GetBitDepth(plane) - 1));
top[..referenceLength].Fill(midpoint);
left[..referenceLength].Fill(midpoint);
return;
}
if (availability.IndexOf(false) < 0)
{
picture.GetRowSpan(plane, y - 1).Slice(x - 1, referenceLength).CopyTo(top);
left[0] = top[0];
for (int i = 1; i < referenceLength; i++)
{
left[i] = picture.GetRowSpan(plane, y + i - 1)[x - 1];
}
return;
}
int leftSampleCount = size * 2;
int lineLength = leftSampleCount + unitWidth + (size * 2);
Span<ushort> line = scratch[..lineLength];
line.Fill((ushort)(1 << (picture.GetBitDepth(plane) - 1)));
// The logical line runs from the bottom-most below-left sample towards the corner and then to the farthest
// above-right sample. This makes substitution a forward fill across availability units.
for (int unit = 0; unit < leftUnitCount; unit++)
{
if (!availability[unit])
{
continue;
}
int sourceY = y + ((leftUnitCount - unit - 1) * unitHeight);
int destinationEnd = ((unit + 1) * unitHeight) - 1;
for (int offset = 0; offset < unitHeight; offset++)
{
line[destinationEnd - offset] = picture.GetRowSpan(plane, sourceY + offset)[x - 1];
}
}
int cornerUnit = leftUnitCount;
if (availability[cornerUnit])
{
line.Slice(leftSampleCount, unitWidth).Fill(picture.GetRowSpan(plane, y - 1)[x - 1]);
}
int topStart = leftSampleCount + unitWidth;
ReadOnlySpan<bool> aboveAvailability = availability.Slice(cornerUnit + 1, aboveUnitCount);
if (aboveAvailability.IndexOf(true) >= 0)
{
// A top-edge block can still have reconstructed left references. Load the preceding row only when
// the availability derivation proves that at least one above or above-right unit exists.
ReadOnlySpan<ushort> aboveRow = picture.GetRowSpan(plane, y - 1);
for (int unit = 0; unit < aboveUnitCount; unit++)
{
if (aboveAvailability[unit])
{
aboveRow.Slice(x + (unit * unitWidth), unitWidth).CopyTo(line.Slice(topStart + (unit * unitWidth), unitWidth));
}
}
}
int firstAvailableUnit = availability.IndexOf(true);
int firstAvailableOffset = firstAvailableUnit < leftUnitCount
? firstAvailableUnit * unitHeight
: leftSampleCount + ((firstAvailableUnit - leftUnitCount) * unitWidth);
int lineOffset = 0;
ushort precedingSample = line[firstAvailableOffset];
for (int unit = 0; unit < totalUnitCount; unit++)
{
int sampleCount = unit < leftUnitCount ? unitHeight : unitWidth;
Span<ushort> unitSamples = line.Slice(lineOffset, sampleCount);
if (!availability[unit])
{
unitSamples.Fill(precedingSample);
}
precedingSample = unitSamples[^1];
lineOffset += sampleCount;
}
int cornerOffset = leftSampleCount + unitWidth - 1;
top[0] = left[0] = line[cornerOffset];
line.Slice(topStart, size * 2).CopyTo(top[1..]);
for (int i = 1; i < referenceLength; i++)
{
left[i] = line[leftSampleCount - i];
}
}
}

387
src/ImageSharp/Formats/Heif/Hevc/HevcIntraPredictor.cs

@ -1,387 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Reconstructs HEVC intra-prediction blocks from prepared neighboring samples.
/// </summary>
/// <remarks>
/// Closed static operators select planar, DC, or angular arithmetic once per block. SIMD rows keep neighboring output
/// columns in consecutive lanes; broadcast left endpoints and vector top references then evaluate the interpolation
/// without per-sample mode dispatch. Horizontal angular prediction reuses the vertical kernel in contiguous scratch
/// storage and transposes once into the strided destination.
/// </remarks>
internal static partial class HevcIntraPredictor
{
/// <summary>
/// The HEVC planar prediction mode.
/// </summary>
private const int PlanarMode = 0;
/// <summary>
/// The HEVC DC prediction mode.
/// </summary>
private const int DcMode = 1;
/// <summary>
/// The HEVC horizontal prediction mode.
/// </summary>
private const int HorizontalMode = 10;
/// <summary>
/// The first prediction mode whose main reference is the top row.
/// </summary>
private const int FirstVerticalMode = 18;
/// <summary>
/// The HEVC vertical prediction mode.
/// </summary>
private const int VerticalMode = 26;
/// <summary>
/// The largest transform-block side supported by HEVC intra prediction.
/// </summary>
private const int MaximumBlockSize = 32;
/// <summary>
/// Gets the angle selected by each absolute angular-mode displacement.
/// </summary>
private static ReadOnlySpan<int> PredictionAngles => [0, 2, 5, 9, 13, 17, 21, 26, 32];
/// <summary>
/// Gets the reciprocal angle used to extend the main reference for negative directions.
/// </summary>
private static ReadOnlySpan<int> InversePredictionAngles => [0, 4096, 1638, 910, 630, 482, 390, 315, 256];
/// <summary>
/// Gets the scratch length required to predict a block of the specified size.
/// </summary>
/// <param name="log2Size">The base-two logarithm of the square block side.</param>
/// <returns>The required number of <see cref="ushort"/> elements.</returns>
public static int GetScratchLength(int log2Size)
{
DebugGuard.MustBeBetweenOrEqualTo(log2Size, 2, 5, nameof(log2Size));
int size = 1 << log2Size;
return (size * size) + (4 * size) + 1;
}
/// <summary>
/// Reconstructs one square intra-prediction block using a closed operator selected by the decoded mode.
/// </summary>
/// <param name="top">The top-left, top, and top-right reference samples.</param>
/// <param name="left">The top-left, left, and below-left reference samples.</param>
/// <param name="destination">The destination buffer beginning at the block origin.</param>
/// <param name="destinationStride">The destination row stride in samples.</param>
/// <param name="log2Size">The base-two logarithm of the square block side.</param>
/// <param name="mode">The decoded prediction mode in the inclusive range zero through thirty-four.</param>
/// <param name="bitDepth">The reconstructed component precision.</param>
/// <param name="filterPredictionEdges">Whether the luma edge filter applies to the selected block.</param>
/// <param name="scratch">The caller-owned scratch returned by <see cref="GetScratchLength(int)"/>.</param>
public static void Predict(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int log2Size,
int mode,
int bitDepth,
bool filterPredictionEdges,
Span<ushort> scratch)
{
DebugGuard.MustBeBetweenOrEqualTo(log2Size, 2, 5, nameof(log2Size));
DebugGuard.MustBeBetweenOrEqualTo(mode, PlanarMode, 34, nameof(mode));
int size = 1 << log2Size;
switch (mode)
{
case PlanarMode:
Predict<PlanarOperator>(
top,
left,
destination,
destinationStride,
size,
mode,
bitDepth,
filterPredictionEdges,
scratch);
break;
case DcMode:
Predict<DcOperator>(
top,
left,
destination,
destinationStride,
size,
mode,
bitDepth,
filterPredictionEdges,
scratch);
break;
default:
Predict<AngularOperator>(
top,
left,
destination,
destinationStride,
size,
mode,
bitDepth,
filterPredictionEdges,
scratch);
break;
}
}
/// <summary>
/// Filters prepared reference samples using the normative three-tap or strong bilinear filter.
/// </summary>
/// <param name="top">The unfiltered top-left, top, and top-right samples.</param>
/// <param name="left">The unfiltered top-left, left, and below-left samples.</param>
/// <param name="filteredTop">The destination top reference.</param>
/// <param name="filteredLeft">The destination left reference.</param>
/// <param name="log2Size">The base-two logarithm of the square prediction-block side.</param>
/// <param name="bitDepth">The reconstructed luma precision.</param>
/// <param name="strongIntraSmoothingEnabled">Whether the sequence permits strong intra smoothing.</param>
public static void FilterReferenceSamples(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> filteredTop,
Span<ushort> filteredLeft,
int log2Size,
int bitDepth,
bool strongIntraSmoothingEnabled)
{
DebugGuard.MustBeBetweenOrEqualTo(log2Size, 2, 5, nameof(log2Size));
int size = 1 << log2Size;
int referenceLength = (size * 2) + 1;
bool useStrongSmoothing = strongIntraSmoothingEnabled && size == MaximumBlockSize;
if (useStrongSmoothing)
{
int threshold = 1 << (bitDepth - 5);
int last = referenceLength - 1;
bool leftIsBilinear = Math.Abs((left[last] + left[0]) - (2 * left[size])) < threshold;
bool topIsBilinear = Math.Abs((top[0] + top[last]) - (2 * top[size])) < threshold;
useStrongSmoothing = leftIsBilinear && topIsBilinear;
}
if (useStrongSmoothing)
{
FilterReferenceBilinear(top[..referenceLength], filteredTop, size);
FilterReferenceBilinear(left[..referenceLength], filteredLeft, size);
return;
}
// The corner belongs to both references. Filtering it once from the first samples on both sides keeps the
// two logical arrays identical at index zero before their independent one-dimensional filters continue.
ushort filteredCorner = (ushort)((left[1] + (2 * top[0]) + top[1] + 2) >> 2);
filteredTop[0] = filteredCorner;
filteredLeft[0] = filteredCorner;
FilterReferenceThreeTap(top[..referenceLength], filteredTop);
FilterReferenceThreeTap(left[..referenceLength], filteredLeft);
}
/// <summary>
/// Invokes one statically selected prediction operator without interface dispatch in the block loop.
/// </summary>
/// <typeparam name="TOperator">The selected prediction operator.</typeparam>
/// <param name="top">The prepared top reference.</param>
/// <param name="left">The prepared left reference.</param>
/// <param name="destination">The destination block origin.</param>
/// <param name="destinationStride">The destination row stride in samples.</param>
/// <param name="size">The square block side in samples.</param>
/// <param name="mode">The decoded prediction mode.</param>
/// <param name="bitDepth">The reconstructed component precision.</param>
/// <param name="filterPredictionEdges">Whether the luma edge filter applies.</param>
/// <param name="scratch">The caller-owned prediction scratch.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void Predict<TOperator>(
ReadOnlySpan<ushort> top,
ReadOnlySpan<ushort> left,
Span<ushort> destination,
int destinationStride,
int size,
int mode,
int bitDepth,
bool filterPredictionEdges,
Span<ushort> scratch)
where TOperator : struct, IHevcIntraPredictionOperator
=> TOperator.Predict(
top,
left,
destination,
destinationStride,
size,
mode,
bitDepth,
filterPredictionEdges,
scratch);
/// <summary>
/// Applies the strong bilinear filter between the reference endpoints.
/// </summary>
/// <param name="source">The complete unfiltered reference.</param>
/// <param name="destination">The complete filtered reference.</param>
/// <param name="size">The prediction-block side in samples.</param>
private static void FilterReferenceBilinear(ReadOnlySpan<ushort> source, Span<ushort> destination, int size)
{
int last = source.Length - 1;
destination[0] = source[0];
destination[last] = source[last];
ref ushort sourceBase = ref MemoryMarshal.GetReference(source);
ref ushort destinationBase = ref MemoryMarshal.GetReference(destination);
uint first = source[0];
uint final = source[last];
int shift = BitOperations.Log2((uint)(size * 2));
uint rounding = (uint)size;
int i = 1;
// Each widened lane represents one reference coordinate. The weights sum to 2N, so narrowing is exact
// after the rounded shift for every supported 8, 10, and 12-bit sample.
if (Vector512.IsHardwareAccelerated)
{
Vector512<uint> indices = CreateIndicesVector512();
int oneVectorFromEnd = last - Vector512<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<ushort>.Count)
{
Vector512<uint> lowerIndices = indices + Vector512.Create((uint)i);
Vector512<uint> upperIndices = lowerIndices + Vector512.Create((uint)Vector512<uint>.Count);
Vector512<uint> lower = (((Vector512.Create((uint)last) - lowerIndices) * first) + (lowerIndices * final) + Vector512.Create(rounding)) >> shift;
Vector512<uint> upper = (((Vector512.Create((uint)last) - upperIndices) * first) + (upperIndices * final) + Vector512.Create(rounding)) >> shift;
Vector512.Narrow(lower, upper).StoreUnsafe(ref Unsafe.Add(ref destinationBase, i));
}
}
if (Vector256.IsHardwareAccelerated)
{
Vector256<uint> indices = CreateIndicesVector256();
int oneVectorFromEnd = last - Vector256<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<ushort>.Count)
{
Vector256<uint> lowerIndices = indices + Vector256.Create((uint)i);
Vector256<uint> upperIndices = lowerIndices + Vector256.Create((uint)Vector256<uint>.Count);
Vector256<uint> lower = (((Vector256.Create((uint)last) - lowerIndices) * first) + (lowerIndices * final) + Vector256.Create(rounding)) >> shift;
Vector256<uint> upper = (((Vector256.Create((uint)last) - upperIndices) * first) + (upperIndices * final) + Vector256.Create(rounding)) >> shift;
Vector256.Narrow(lower, upper).StoreUnsafe(ref Unsafe.Add(ref destinationBase, i));
}
}
if (Vector128.IsHardwareAccelerated)
{
Vector128<uint> indices = CreateIndicesVector128();
int oneVectorFromEnd = last - Vector128<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<ushort>.Count)
{
Vector128<uint> lowerIndices = indices + Vector128.Create((uint)i);
Vector128<uint> upperIndices = lowerIndices + Vector128.Create((uint)Vector128<uint>.Count);
Vector128<uint> lower = (((Vector128.Create((uint)last) - lowerIndices) * first) + (lowerIndices * final) + Vector128.Create(rounding)) >> shift;
Vector128<uint> upper = (((Vector128.Create((uint)last) - upperIndices) * first) + (upperIndices * final) + Vector128.Create(rounding)) >> shift;
Vector128.Narrow(lower, upper).StoreUnsafe(ref Unsafe.Add(ref destinationBase, i));
}
}
for (; i < last; i++)
{
Unsafe.Add(ref destinationBase, i) = (ushort)((((last - i) * first) + (i * final) + rounding) >> shift);
}
}
/// <summary>
/// Applies the normal three-tap reference filter to every non-endpoint sample.
/// </summary>
/// <param name="source">The complete unfiltered reference.</param>
/// <param name="destination">The complete filtered reference with its corner already initialized.</param>
private static void FilterReferenceThreeTap(ReadOnlySpan<ushort> source, Span<ushort> destination)
{
int last = source.Length - 1;
destination[last] = source[last];
ref ushort sourceBase = ref MemoryMarshal.GetReference(source);
ref ushort destinationBase = ref MemoryMarshal.GetReference(destination);
int i = 1;
if (Vector512.IsHardwareAccelerated)
{
int oneVectorFromEnd = last - Vector512<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<ushort>.Count)
{
Vector512<ushort> previous = Vector512.LoadUnsafe(ref sourceBase, (nuint)(i - 1));
Vector512<ushort> current = Vector512.LoadUnsafe(ref sourceBase, (nuint)i);
Vector512<ushort> next = Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + 1));
(Vector512<uint> previousLow, Vector512<uint> previousHigh) = Vector512.Widen(previous);
(Vector512<uint> currentLow, Vector512<uint> currentHigh) = Vector512.Widen(current);
(Vector512<uint> nextLow, Vector512<uint> nextHigh) = Vector512.Widen(next);
Vector512<uint> low = (previousLow + (currentLow << 1) + nextLow + Vector512.Create(2U)) >> 2;
Vector512<uint> high = (previousHigh + (currentHigh << 1) + nextHigh + Vector512.Create(2U)) >> 2;
Vector512.Narrow(low, high).StoreUnsafe(ref Unsafe.Add(ref destinationBase, i));
}
}
if (Vector256.IsHardwareAccelerated)
{
int oneVectorFromEnd = last - Vector256<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<ushort>.Count)
{
Vector256<ushort> previous = Vector256.LoadUnsafe(ref sourceBase, (nuint)(i - 1));
Vector256<ushort> current = Vector256.LoadUnsafe(ref sourceBase, (nuint)i);
Vector256<ushort> next = Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + 1));
(Vector256<uint> previousLow, Vector256<uint> previousHigh) = Vector256.Widen(previous);
(Vector256<uint> currentLow, Vector256<uint> currentHigh) = Vector256.Widen(current);
(Vector256<uint> nextLow, Vector256<uint> nextHigh) = Vector256.Widen(next);
Vector256<uint> low = (previousLow + (currentLow << 1) + nextLow + Vector256.Create(2U)) >> 2;
Vector256<uint> high = (previousHigh + (currentHigh << 1) + nextHigh + Vector256.Create(2U)) >> 2;
Vector256.Narrow(low, high).StoreUnsafe(ref Unsafe.Add(ref destinationBase, i));
}
}
if (Vector128.IsHardwareAccelerated)
{
int oneVectorFromEnd = last - Vector128<ushort>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<ushort>.Count)
{
Vector128<ushort> previous = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i - 1));
Vector128<ushort> current = Vector128.LoadUnsafe(ref sourceBase, (nuint)i);
Vector128<ushort> next = Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + 1));
(Vector128<uint> previousLow, Vector128<uint> previousHigh) = Vector128.Widen(previous);
(Vector128<uint> currentLow, Vector128<uint> currentHigh) = Vector128.Widen(current);
(Vector128<uint> nextLow, Vector128<uint> nextHigh) = Vector128.Widen(next);
Vector128<uint> low = (previousLow + (currentLow << 1) + nextLow + Vector128.Create(2U)) >> 2;
Vector128<uint> high = (previousHigh + (currentHigh << 1) + nextHigh + Vector128.Create(2U)) >> 2;
Vector128.Narrow(low, high).StoreUnsafe(ref Unsafe.Add(ref destinationBase, i));
}
}
for (; i < last; i++)
{
Unsafe.Add(ref destinationBase, i) = (ushort)((source[i - 1] + (2 * source[i]) + source[i + 1] + 2) >> 2);
}
}
/// <summary>
/// Creates the zero-through-fifteen lane indices used by 512-bit weighted interpolation.
/// </summary>
/// <returns>The ordered lane indices.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<uint> CreateIndicesVector512()
=> Vector512.Create(0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U, 9U, 10U, 11U, 12U, 13U, 14U, 15U);
/// <summary>
/// Creates the zero-through-seven lane indices used by 256-bit weighted interpolation.
/// </summary>
/// <returns>The ordered lane indices.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<uint> CreateIndicesVector256() => Vector256.Create(0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U);
/// <summary>
/// Creates the zero-through-three lane indices used by 128-bit weighted interpolation.
/// </summary>
/// <returns>The ordered lane indices.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<uint> CreateIndicesVector128() => Vector128.Create(0U, 1U, 2U, 3U);
}

412
src/ImageSharp/Formats/Heif/Hevc/HevcInverseQuantizer.cs

@ -1,412 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Reconstructs dequantized HEVC transform coefficients.
/// </summary>
/// <remarks>
/// Consecutive quantized coefficients occupy consecutive 32-bit lanes. Scaling-list values are widened to the same lane
/// shape before the inverse scale, quantization shift, rounding, and transform-range clamp are applied. Vector-width
/// loops advance the complete coefficient prefix and leave only the final incomplete group to the scalar equation.
/// </remarks>
internal static class HevcInverseQuantizer
{
/// <summary>
/// Gets the inverse quantization scale selected by the quantization-parameter remainder.
/// </summary>
private static ReadOnlySpan<byte> InverseQuantizationScales => [40, 45, 51, 57, 64, 72];
/// <summary>
/// Dequantizes one square transform block using the effective component quantization parameter.
/// </summary>
/// <param name="quantized">The decoded quantized coefficients in raster order.</param>
/// <param name="destination">The destination dequantized coefficients in raster order.</param>
/// <param name="log2Size">The base-two logarithm of the transform-block side.</param>
/// <param name="bitDepth">The reconstructed component precision.</param>
/// <param name="maxTransformDynamicRange">The transform dynamic range excluding its sign bit.</param>
/// <param name="quantizationParameter">The effective nonnegative component quantization parameter including its bit-depth offset.</param>
/// <param name="scalingListEnabled">Whether the governing sequence enables scaling lists.</param>
/// <param name="scalingList">The effective picture scaling matrices.</param>
/// <param name="plane">The reconstructed color plane.</param>
/// <param name="isIntraPredicted">Whether the transform block belongs to an intra-predicted coding unit.</param>
/// <param name="transformSkip">Whether the transform block bypasses the inverse transform.</param>
/// <param name="extendedPrecisionProcessingEnabled">Whether transform-skip precision is extended by the sequence.</param>
public static void Dequantize(
ReadOnlySpan<int> quantized,
Span<int> destination,
int log2Size,
int bitDepth,
int maxTransformDynamicRange,
int quantizationParameter,
bool scalingListEnabled,
HevcScalingList scalingList,
HevcPlane plane,
bool isIntraPredicted,
bool transformSkip,
bool extendedPrecisionProcessingEnabled)
{
DebugGuard.MustBeBetweenOrEqualTo(log2Size, 2, 5, nameof(log2Size));
DebugGuard.MustBeGreaterThanOrEqualTo(quantizationParameter, 0, nameof(quantizationParameter));
int size = 1 << log2Size;
int sampleCount = size * size;
DebugGuard.IsTrue(quantized.Length >= sampleCount, "The quantized coefficient span is shorter than the transform block.");
DebugGuard.IsTrue(destination.Length >= sampleCount, "The dequantized coefficient span is shorter than the transform block.");
int transformShift = maxTransformDynamicRange - bitDepth - log2Size;
if (transformSkip && extendedPrecisionProcessingEnabled)
{
transformShift = Math.Max(0, transformShift);
}
int quantizationParameterPer = quantizationParameter / 6;
int quantizationParameterRemainder = quantizationParameter % 6;
int inverseQuantizationScale = InverseQuantizationScales[quantizationParameterRemainder];
bool useScalingList = scalingListEnabled && (!transformSkip || log2Size == 2);
int rightShift = 6 - (transformShift + quantizationParameterPer) + (useScalingList ? 4 : 0);
int outputMinimum = -(1 << maxTransformDynamicRange);
int outputMaximum = (1 << maxTransformDynamicRange) - 1;
// The input clip is part of the normative dequantization process. Its right-shift dependency ensures the
// following signed 32-bit multiplication and optional left shift cannot overflow for any valid coefficient.
int scaleBits = useScalingList ? 15 : 7;
int targetInputBitDepth = Math.Min(maxTransformDynamicRange + 1, 32 + rightShift - scaleBits);
int inputMinimum = -(1 << (targetInputBitDepth - 1));
int inputMaximum = (1 << (targetInputBitDepth - 1)) - 1;
if (!useScalingList)
{
DequantizeUniform(
quantized[..sampleCount],
destination[..sampleCount],
inverseQuantizationScale,
rightShift,
inputMinimum,
inputMaximum,
outputMinimum,
outputMaximum);
return;
}
int sizeId = log2Size - 2;
int matrixId = (isIntraPredicted ? 0 : 3) + (int)plane;
ReadOnlySpan<byte> matrix = scalingList.GetExpandedMatrix(sizeId, matrixId);
DequantizeScalingList(
quantized[..sampleCount],
destination[..sampleCount],
matrix,
inverseQuantizationScale,
rightShift,
inputMinimum,
inputMaximum,
outputMinimum,
outputMaximum);
}
/// <summary>
/// Dequantizes coefficients using one uniform inverse-quantization scale.
/// </summary>
/// <param name="source">The complete quantized coefficient block.</param>
/// <param name="destination">The complete dequantized coefficient block.</param>
/// <param name="scale">The inverse-quantization scale.</param>
/// <param name="rightShift">The signed normalization shift.</param>
/// <param name="inputMinimum">The inclusive quantized input minimum.</param>
/// <param name="inputMaximum">The inclusive quantized input maximum.</param>
/// <param name="outputMinimum">The inclusive dequantized output minimum.</param>
/// <param name="outputMaximum">The inclusive dequantized output maximum.</param>
private static void DequantizeUniform(
ReadOnlySpan<int> source,
Span<int> destination,
int scale,
int rightShift,
int inputMinimum,
int inputMaximum,
int outputMinimum,
int outputMaximum)
{
ref int sourceBase = ref MemoryMarshal.GetReference(source);
ref int destinationBase = ref MemoryMarshal.GetReference(destination);
int i = 0;
// Descending widths advance one shared coefficient offset. Smaller registers consume complete groups left by a
// wider path, so the scalar loop sees fewer than four values without any overlapping dequantization stores.
if (Vector512.IsHardwareAccelerated)
{
int oneVectorFromEnd = source.Length - Vector512<int>.Count;
Vector512<int> weights = Vector512.Create(scale);
for (; i <= oneVectorFromEnd; i += Vector512<int>.Count)
{
Vector512<int> values = Vector512.LoadUnsafe(ref sourceBase, (nuint)i);
Dequantize(values, weights, rightShift, inputMinimum, inputMaximum, outputMinimum, outputMaximum).StoreUnsafe(ref destinationBase, (nuint)i);
}
}
if (Vector256.IsHardwareAccelerated)
{
int oneVectorFromEnd = source.Length - Vector256<int>.Count;
Vector256<int> weights = Vector256.Create(scale);
for (; i <= oneVectorFromEnd; i += Vector256<int>.Count)
{
Vector256<int> values = Vector256.LoadUnsafe(ref sourceBase, (nuint)i);
Dequantize(values, weights, rightShift, inputMinimum, inputMaximum, outputMinimum, outputMaximum).StoreUnsafe(ref destinationBase, (nuint)i);
}
}
if (Vector128.IsHardwareAccelerated)
{
int oneVectorFromEnd = source.Length - Vector128<int>.Count;
Vector128<int> weights = Vector128.Create(scale);
for (; i <= oneVectorFromEnd; i += Vector128<int>.Count)
{
Vector128<int> values = Vector128.LoadUnsafe(ref sourceBase, (nuint)i);
Dequantize(values, weights, rightShift, inputMinimum, inputMaximum, outputMinimum, outputMaximum).StoreUnsafe(ref destinationBase, (nuint)i);
}
}
for (; i < source.Length; i++)
{
destination[i] = Dequantize(source[i], scale, rightShift, inputMinimum, inputMaximum, outputMinimum, outputMaximum);
}
}
/// <summary>
/// Dequantizes coefficients using the expanded scaling matrix selected for the transform block.
/// </summary>
/// <param name="source">The complete quantized coefficient block.</param>
/// <param name="destination">The complete dequantized coefficient block.</param>
/// <param name="matrix">The scaling matrix expanded to the transform dimensions.</param>
/// <param name="inverseQuantizationScale">The inverse-quantization scale.</param>
/// <param name="rightShift">The signed normalization shift.</param>
/// <param name="inputMinimum">The inclusive quantized input minimum.</param>
/// <param name="inputMaximum">The inclusive quantized input maximum.</param>
/// <param name="outputMinimum">The inclusive dequantized output minimum.</param>
/// <param name="outputMaximum">The inclusive dequantized output maximum.</param>
private static void DequantizeScalingList(
ReadOnlySpan<int> source,
Span<int> destination,
ReadOnlySpan<byte> matrix,
int inverseQuantizationScale,
int rightShift,
int inputMinimum,
int inputMaximum,
int outputMinimum,
int outputMaximum)
{
ref int sourceBase = ref MemoryMarshal.GetReference(source);
ref int destinationBase = ref MemoryMarshal.GetReference(destination);
ref byte matrixBase = ref MemoryMarshal.GetReference(matrix);
int i = 0;
// Scaling matrices use one unsigned byte per coefficient. Each width loads exactly its matching byte count,
// widens in source order, and multiplies by the common inverse-quantization scale before signed arithmetic.
if (Vector512.IsHardwareAccelerated)
{
int oneVectorFromEnd = source.Length - Vector512<int>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<int>.Count)
{
Vector512<int> values = Vector512.LoadUnsafe(ref sourceBase, (nuint)i);
Vector512<int> weights = LoadScalingWeightsVector512(ref Unsafe.Add(ref matrixBase, i), inverseQuantizationScale);
Dequantize(values, weights, rightShift, inputMinimum, inputMaximum, outputMinimum, outputMaximum).StoreUnsafe(ref destinationBase, (nuint)i);
}
}
if (Vector256.IsHardwareAccelerated)
{
int oneVectorFromEnd = source.Length - Vector256<int>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<int>.Count)
{
Vector256<int> values = Vector256.LoadUnsafe(ref sourceBase, (nuint)i);
Vector256<int> weights = LoadScalingWeightsVector256(ref Unsafe.Add(ref matrixBase, i), inverseQuantizationScale);
Dequantize(values, weights, rightShift, inputMinimum, inputMaximum, outputMinimum, outputMaximum).StoreUnsafe(ref destinationBase, (nuint)i);
}
}
if (Vector128.IsHardwareAccelerated)
{
int oneVectorFromEnd = source.Length - Vector128<int>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<int>.Count)
{
Vector128<int> values = Vector128.LoadUnsafe(ref sourceBase, (nuint)i);
Vector128<int> weights = LoadScalingWeightsVector128(ref Unsafe.Add(ref matrixBase, i), inverseQuantizationScale);
Dequantize(values, weights, rightShift, inputMinimum, inputMaximum, outputMinimum, outputMaximum).StoreUnsafe(ref destinationBase, (nuint)i);
}
}
for (; i < source.Length; i++)
{
int weight = Unsafe.Add(ref matrixBase, i) * inverseQuantizationScale;
destination[i] = Dequantize(source[i], weight, rightShift, inputMinimum, inputMaximum, outputMinimum, outputMaximum);
}
}
/// <summary>
/// Loads and widens sixteen scaling coefficients for a 512-bit coefficient vector.
/// </summary>
/// <param name="source">The first scaling coefficient.</param>
/// <param name="scale">The inverse-quantization scale.</param>
/// <returns>The sixteen ordered dequantization weights.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<int> LoadScalingWeightsVector512(ref byte source, int scale)
{
Vector128<byte> packed = Vector128.LoadUnsafe(ref source);
(Vector128<ushort> lower16, Vector128<ushort> upper16) = Vector128.Widen(packed);
Vector256<uint> lower32 = Vector256.Create(Vector128.WidenLower(lower16), Vector128.WidenUpper(lower16));
Vector256<uint> upper32 = Vector256.Create(Vector128.WidenLower(upper16), Vector128.WidenUpper(upper16));
return Vector512.Create(lower32, upper32).AsInt32() * Vector512.Create(scale);
}
/// <summary>
/// Loads and widens eight scaling coefficients for a 256-bit coefficient vector.
/// </summary>
/// <param name="source">The first scaling coefficient.</param>
/// <param name="scale">The inverse-quantization scale.</param>
/// <returns>The eight ordered dequantization weights.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<int> LoadScalingWeightsVector256(ref byte source, int scale)
{
ulong packed = Unsafe.ReadUnaligned<ulong>(ref source);
Vector128<ushort> values16 = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte());
Vector256<uint> values32 = Vector256.Create(Vector128.WidenLower(values16), Vector128.WidenUpper(values16));
return values32.AsInt32() * Vector256.Create(scale);
}
/// <summary>
/// Loads and widens four scaling coefficients for a 128-bit coefficient vector.
/// </summary>
/// <param name="source">The first scaling coefficient.</param>
/// <param name="scale">The inverse-quantization scale.</param>
/// <returns>The four ordered dequantization weights.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<int> LoadScalingWeightsVector128(ref byte source, int scale)
{
uint packed = Unsafe.ReadUnaligned<uint>(ref source);
Vector128<ushort> values16 = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte());
Vector128<uint> values32 = Vector128.WidenLower(values16);
return values32.AsInt32() * Vector128.Create(scale);
}
/// <summary>
/// Dequantizes sixteen signed coefficients with independent scaling weights.
/// </summary>
/// <param name="values">The quantized coefficients.</param>
/// <param name="weights">The dequantization weights.</param>
/// <param name="rightShift">The signed normalization shift.</param>
/// <param name="inputMinimum">The inclusive quantized input minimum.</param>
/// <param name="inputMaximum">The inclusive quantized input maximum.</param>
/// <param name="outputMinimum">The inclusive dequantized output minimum.</param>
/// <param name="outputMaximum">The inclusive dequantized output maximum.</param>
/// <returns>The dequantized coefficients.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<int> Dequantize(
Vector512<int> values,
Vector512<int> weights,
int rightShift,
int inputMinimum,
int inputMaximum,
int outputMinimum,
int outputMaximum)
{
// A positive rightShift applies nearest-integer rounding before division. A nonpositive value represents an
// exact left shift; the earlier input clamp guarantees that multiplication and shifting remain in Int32 range.
values = Vector512.Clamp(values, Vector512.Create(inputMinimum), Vector512.Create(inputMaximum));
Vector512<int> result = values * weights;
result = rightShift > 0
? (result + Vector512.Create(1 << (rightShift - 1))) >> rightShift
: result << -rightShift;
return Vector512.Clamp(result, Vector512.Create(outputMinimum), Vector512.Create(outputMaximum));
}
/// <summary>
/// Dequantizes eight signed coefficients with independent scaling weights.
/// </summary>
/// <param name="values">The quantized coefficients.</param>
/// <param name="weights">The dequantization weights.</param>
/// <param name="rightShift">The signed normalization shift.</param>
/// <param name="inputMinimum">The inclusive quantized input minimum.</param>
/// <param name="inputMaximum">The inclusive quantized input maximum.</param>
/// <param name="outputMinimum">The inclusive dequantized output minimum.</param>
/// <param name="outputMaximum">The inclusive dequantized output maximum.</param>
/// <returns>The dequantized coefficients.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<int> Dequantize(
Vector256<int> values,
Vector256<int> weights,
int rightShift,
int inputMinimum,
int inputMaximum,
int outputMinimum,
int outputMaximum)
{
values = Vector256.Clamp(values, Vector256.Create(inputMinimum), Vector256.Create(inputMaximum));
Vector256<int> result = values * weights;
result = rightShift > 0
? (result + Vector256.Create(1 << (rightShift - 1))) >> rightShift
: result << -rightShift;
return Vector256.Clamp(result, Vector256.Create(outputMinimum), Vector256.Create(outputMaximum));
}
/// <summary>
/// Dequantizes four signed coefficients with independent scaling weights.
/// </summary>
/// <param name="values">The quantized coefficients.</param>
/// <param name="weights">The dequantization weights.</param>
/// <param name="rightShift">The signed normalization shift.</param>
/// <param name="inputMinimum">The inclusive quantized input minimum.</param>
/// <param name="inputMaximum">The inclusive quantized input maximum.</param>
/// <param name="outputMinimum">The inclusive dequantized output minimum.</param>
/// <param name="outputMaximum">The inclusive dequantized output maximum.</param>
/// <returns>The dequantized coefficients.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<int> Dequantize(
Vector128<int> values,
Vector128<int> weights,
int rightShift,
int inputMinimum,
int inputMaximum,
int outputMinimum,
int outputMaximum)
{
values = Vector128.Clamp(values, Vector128.Create(inputMinimum), Vector128.Create(inputMaximum));
Vector128<int> result = values * weights;
result = rightShift > 0
? (result + Vector128.Create(1 << (rightShift - 1))) >> rightShift
: result << -rightShift;
return Vector128.Clamp(result, Vector128.Create(outputMinimum), Vector128.Create(outputMaximum));
}
/// <summary>
/// Dequantizes one signed coefficient.
/// </summary>
/// <param name="value">The quantized coefficient.</param>
/// <param name="weight">The dequantization weight.</param>
/// <param name="rightShift">The signed normalization shift.</param>
/// <param name="inputMinimum">The inclusive quantized input minimum.</param>
/// <param name="inputMaximum">The inclusive quantized input maximum.</param>
/// <param name="outputMinimum">The inclusive dequantized output minimum.</param>
/// <param name="outputMaximum">The inclusive dequantized output maximum.</param>
/// <returns>The dequantized coefficient.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int Dequantize(
int value,
int weight,
int rightShift,
int inputMinimum,
int inputMaximum,
int outputMinimum,
int outputMaximum)
{
int result = Math.Clamp(value, inputMinimum, inputMaximum) * weight;
result = rightShift > 0 ? (result + (1 << (rightShift - 1))) >> rightShift : result << -rightShift;
return Math.Clamp(result, outputMinimum, outputMaximum);
}
}

49
src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.DiscreteCosine16Operator.cs

@ -1,49 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Defines the sixteen-point inverse discrete cosine transform operator.
/// </content>
internal static partial class HevcInverseTransformer
{
/// <summary>
/// Implements the sixteen-point inverse discrete cosine transform.
/// </summary>
private readonly struct DiscreteCosine16Operator : IHevcInverseTransformOperator
{
/// <inheritdoc/>
public static int Size => 16;
/// <inheritdoc/>
public static bool UsesButterfly => true;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetCoefficient(int frequency, int position)
{
if (frequency == 0)
{
return 64;
}
int angle = ((2 * position) + 1) * frequency * 2;
angle &= 127;
if (angle > 64)
{
angle = 128 - angle;
}
// The second quadrant reuses the first-quadrant magnitude with a negative sign.
if (angle > 32)
{
return -DiscreteCosineMagnitudes[64 - angle];
}
return DiscreteCosineMagnitudes[angle];
}
}
}

49
src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.DiscreteCosine32Operator.cs

@ -1,49 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Defines the thirty-two-point inverse discrete cosine transform operator.
/// </content>
internal static partial class HevcInverseTransformer
{
/// <summary>
/// Implements the thirty-two-point inverse discrete cosine transform.
/// </summary>
private readonly struct DiscreteCosine32Operator : IHevcInverseTransformOperator
{
/// <inheritdoc/>
public static int Size => 32;
/// <inheritdoc/>
public static bool UsesButterfly => true;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetCoefficient(int frequency, int position)
{
if (frequency == 0)
{
return 64;
}
int angle = ((2 * position) + 1) * frequency * 1;
angle &= 127;
if (angle > 64)
{
angle = 128 - angle;
}
// The second quadrant reuses the first-quadrant magnitude with a negative sign.
if (angle > 32)
{
return -DiscreteCosineMagnitudes[64 - angle];
}
return DiscreteCosineMagnitudes[angle];
}
}
}

49
src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.DiscreteCosine4Operator.cs

@ -1,49 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Defines the four-point inverse discrete cosine transform operator.
/// </content>
internal static partial class HevcInverseTransformer
{
/// <summary>
/// Implements the four-point inverse discrete cosine transform.
/// </summary>
private readonly struct DiscreteCosine4Operator : IHevcInverseTransformOperator
{
/// <inheritdoc/>
public static int Size => 4;
/// <inheritdoc/>
public static bool UsesButterfly => true;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetCoefficient(int frequency, int position)
{
if (frequency == 0)
{
return 64;
}
int angle = ((2 * position) + 1) * frequency * 8;
angle &= 127;
if (angle > 64)
{
angle = 128 - angle;
}
// The second quadrant reuses the first-quadrant magnitude with a negative sign.
if (angle > 32)
{
return -DiscreteCosineMagnitudes[64 - angle];
}
return DiscreteCosineMagnitudes[angle];
}
}
}

49
src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.DiscreteCosine8Operator.cs

@ -1,49 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Defines the eight-point inverse discrete cosine transform operator.
/// </content>
internal static partial class HevcInverseTransformer
{
/// <summary>
/// Implements the eight-point inverse discrete cosine transform.
/// </summary>
private readonly struct DiscreteCosine8Operator : IHevcInverseTransformOperator
{
/// <inheritdoc/>
public static int Size => 8;
/// <inheritdoc/>
public static bool UsesButterfly => true;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetCoefficient(int frequency, int position)
{
if (frequency == 0)
{
return 64;
}
int angle = ((2 * position) + 1) * frequency * 4;
angle &= 127;
if (angle > 64)
{
angle = 128 - angle;
}
// The second quadrant reuses the first-quadrant magnitude with a negative sign.
if (angle > 32)
{
return -DiscreteCosineMagnitudes[64 - angle];
}
return DiscreteCosineMagnitudes[angle];
}
}
}

42
src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.DiscreteSine4Operator.cs

@ -1,42 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Defines the four-point inverse discrete sine transform operator.
/// </content>
internal static partial class HevcInverseTransformer
{
/// <summary>
/// Implements the four-point inverse discrete sine transform.
/// </summary>
private readonly struct DiscreteSine4Operator : IHevcInverseTransformOperator
{
/// <summary>
/// Gets the inverse-DST matrix in frequency-major order.
/// </summary>
private static ReadOnlySpan<sbyte> Coefficients =>
[
29, 55, 74, 84,
74, 74, 0, -74,
84, -29, -74, 55,
55, -84, 74, -29
];
/// <inheritdoc/>
public static int Size => 4;
/// <inheritdoc/>
public static bool UsesButterfly => false;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetCoefficient(int frequency, int position)
{
return Coefficients[(frequency * Size) + position];
}
}
}

547
src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.Operations.cs

@ -1,547 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Provides the shared HEVC inverse-transform stage and transpose operations. Vector lanes represent independent
/// transform lines, while consecutive scratch rows represent frequency groups in the partial-butterfly factorization.
/// Arithmetic never mixes lines; transposition is the only operation that exchanges row and column coordinates.
/// </content>
internal static partial class HevcInverseTransformer
{
/// <summary>
/// Gets the common inverse-DCT magnitudes ordered on the pi-over-sixty-four angle grid.
/// </summary>
private static ReadOnlySpan<sbyte> DiscreteCosineMagnitudes =>
[
90, 90, 90, 90, 89, 88, 87, 85, 83, 82, 80, 78, 75, 73, 70, 67, 64,
61, 57, 54, 50, 46, 43, 38, 36, 31, 25, 22, 18, 13, 9, 4, 0
];
/// <summary>
/// Calculates the disjoint odd-frequency groups that seed the HEVC partial-butterfly reconstruction.
/// </summary>
/// <typeparam name="TOperator">The selected inverse-DCT operator.</typeparam>
/// <param name="source">The frequency rows followed by contiguous independent lines.</param>
/// <param name="groups">The destination group rows.</param>
/// <param name="lineCount">The number of independent lines transformed together.</param>
private static void PopulateButterflyGroups<TOperator>(ReadOnlySpan<int> source, Span<int> groups, int lineCount)
where TOperator : struct, IHevcInverseTransformOperator
{
int size = TOperator.Size;
int groupOffset = 0;
for (int frequencyStep = 2; frequencyStep < size; frequencyStep <<= 1)
{
int outputCount = size / frequencyStep;
int firstFrequency = frequencyStep >> 1;
for (int position = 0; position < outputCount; position++)
{
PopulateButterflyGroupRow<TOperator>(
source,
groups.Slice((groupOffset + position) * lineCount, lineCount),
lineCount,
firstFrequency,
frequencyStep,
position);
}
groupOffset += outputCount;
}
// The deepest even group contains the DC term and the transform's Nyquist-frequency term. It remains a
// two-element group for every supported DCT size and closes the recursive butterfly hierarchy.
for (int position = 0; position < 2; position++)
{
PopulateButterflyGroupRow<TOperator>(
source,
groups.Slice((groupOffset + position) * lineCount, lineCount),
lineCount,
0,
size >> 1,
position);
}
}
/// <summary>
/// Calculates one partial-butterfly group row across all independent lines.
/// </summary>
/// <typeparam name="TOperator">The selected inverse-DCT operator.</typeparam>
/// <param name="source">The complete frequency-row input.</param>
/// <param name="destination">The destination group row.</param>
/// <param name="lineCount">The number of independent lines.</param>
/// <param name="firstFrequency">The first frequency included by the group.</param>
/// <param name="frequencyStep">The distance between included frequencies.</param>
/// <param name="position">The group-relative spatial coordinate.</param>
private static void PopulateButterflyGroupRow<TOperator>(
ReadOnlySpan<int> source,
Span<int> destination,
int lineCount,
int firstFrequency,
int frequencyStep,
int position)
where TOperator : struct, IHevcInverseTransformOperator
{
ref int sourceBase = ref MemoryMarshal.GetReference(source);
ref int destinationBase = ref MemoryMarshal.GetReference(destination);
int x = 0;
// Source storage is frequency-major: advancing one lane moves to the same frequency in another independent
// transform line. The shared X offset lets each narrower width continue exactly where the wider loop stopped.
if (Vector512.IsHardwareAccelerated)
{
int oneVectorFromEnd = lineCount - Vector512<int>.Count;
for (; x <= oneVectorFromEnd; x += Vector512<int>.Count)
{
Vector512<int> sum = Vector512<int>.Zero;
for (int frequency = firstFrequency; frequency < TOperator.Size; frequency += frequencyStep)
{
Vector512<int> values = Vector512.LoadUnsafe(ref sourceBase, (nuint)((frequency * lineCount) + x));
sum += values * Vector512.Create(TOperator.GetCoefficient(frequency, position));
}
sum.StoreUnsafe(ref destinationBase, (nuint)x);
}
}
if (Vector256.IsHardwareAccelerated)
{
int oneVectorFromEnd = lineCount - Vector256<int>.Count;
for (; x <= oneVectorFromEnd; x += Vector256<int>.Count)
{
Vector256<int> sum = Vector256<int>.Zero;
for (int frequency = firstFrequency; frequency < TOperator.Size; frequency += frequencyStep)
{
Vector256<int> values = Vector256.LoadUnsafe(ref sourceBase, (nuint)((frequency * lineCount) + x));
sum += values * Vector256.Create(TOperator.GetCoefficient(frequency, position));
}
sum.StoreUnsafe(ref destinationBase, (nuint)x);
}
}
if (Vector128.IsHardwareAccelerated)
{
int oneVectorFromEnd = lineCount - Vector128<int>.Count;
for (; x <= oneVectorFromEnd; x += Vector128<int>.Count)
{
Vector128<int> sum = Vector128<int>.Zero;
for (int frequency = firstFrequency; frequency < TOperator.Size; frequency += frequencyStep)
{
Vector128<int> values = Vector128.LoadUnsafe(ref sourceBase, (nuint)((frequency * lineCount) + x));
sum += values * Vector128.Create(TOperator.GetCoefficient(frequency, position));
}
sum.StoreUnsafe(ref destinationBase, (nuint)x);
}
}
for (; x < lineCount; x++)
{
int sum = 0;
for (int frequency = firstFrequency; frequency < TOperator.Size; frequency += frequencyStep)
{
sum += source[(frequency * lineCount) + x] * TOperator.GetCoefficient(frequency, position);
}
destination[x] = sum;
}
}
/// <summary>
/// Expands the disjoint partial-butterfly groups into spatial rows.
/// </summary>
/// <typeparam name="TOperator">The selected inverse-DCT operator.</typeparam>
/// <param name="initial">The buffer containing every disjoint group.</param>
/// <param name="alternate">The alternate expansion buffer.</param>
/// <param name="lineCount">The number of independent lines transformed together.</param>
/// <param name="shift">The rounded right shift applied at the final hierarchy level.</param>
/// <param name="minimum">The inclusive output minimum.</param>
/// <param name="maximum">The inclusive output maximum.</param>
/// <returns>The buffer containing the completed spatial rows.</returns>
private static Span<int> CombineButterflyGroups<TOperator>(
Span<int> initial,
Span<int> alternate,
int lineCount,
int shift,
int minimum,
int maximum)
where TOperator : struct, IHevcInverseTransformOperator
{
int size = TOperator.Size;
int combinedSize = 2;
int combinedStart = size - combinedSize;
bool currentIsInitial = true;
while (combinedSize < size)
{
int oddStart = combinedStart - combinedSize;
bool finalLevel = (combinedSize << 1) == size;
ReadOnlySpan<int> even = currentIsInitial ? initial : alternate;
Span<int> destination = currentIsInitial ? alternate : initial;
// Odd rows remain in the initial disjoint-group buffer while expanded even rows alternate buffers. This
// preserves every source row needed by later hierarchy levels without allocating another transform block.
for (int position = 0; position < combinedSize; position++)
{
ReadOnlySpan<int> evenRow = even.Slice((combinedStart + position) * lineCount, lineCount);
ReadOnlySpan<int> oddRow = initial.Slice((oddStart + position) * lineCount, lineCount);
Span<int> positiveRow = destination.Slice((oddStart + position) * lineCount, lineCount);
Span<int> negativeRow = destination.Slice((oddStart + (2 * combinedSize) - 1 - position) * lineCount, lineCount);
CombineButterflyRows(evenRow, oddRow, positiveRow, negativeRow, finalLevel, shift, minimum, maximum);
}
combinedStart = oddStart;
combinedSize <<= 1;
currentIsInitial = !currentIsInitial;
}
return currentIsInitial ? initial : alternate;
}
/// <summary>
/// Combines one symmetric pair of partial-butterfly rows.
/// </summary>
/// <param name="even">The even-frequency contribution.</param>
/// <param name="odd">The odd-frequency contribution.</param>
/// <param name="positive">The destination receiving the added contribution.</param>
/// <param name="negative">The destination receiving the subtracted contribution.</param>
/// <param name="roundAndClip">Whether this is the final hierarchy level.</param>
/// <param name="shift">The rounded right shift applied at the final hierarchy level.</param>
/// <param name="minimum">The inclusive output minimum.</param>
/// <param name="maximum">The inclusive output maximum.</param>
private static void CombineButterflyRows(
ReadOnlySpan<int> even,
ReadOnlySpan<int> odd,
Span<int> positive,
Span<int> negative,
bool roundAndClip,
int shift,
int minimum,
int maximum)
{
ref int evenBase = ref MemoryMarshal.GetReference(even);
ref int oddBase = ref MemoryMarshal.GetReference(odd);
ref int positiveBase = ref MemoryMarshal.GetReference(positive);
ref int negativeBase = ref MemoryMarshal.GetReference(negative);
int x = 0;
if (Vector512.IsHardwareAccelerated)
{
int oneVectorFromEnd = even.Length - Vector512<int>.Count;
for (; x <= oneVectorFromEnd; x += Vector512<int>.Count)
{
Vector512<int> evenValues = Vector512.LoadUnsafe(ref evenBase, (nuint)x);
Vector512<int> oddValues = Vector512.LoadUnsafe(ref oddBase, (nuint)x);
Vector512<int> added = evenValues + oddValues;
Vector512<int> subtracted = evenValues - oddValues;
if (roundAndClip)
{
added = RoundShiftAndClamp(added, shift, minimum, maximum);
subtracted = RoundShiftAndClamp(subtracted, shift, minimum, maximum);
}
added.StoreUnsafe(ref positiveBase, (nuint)x);
subtracted.StoreUnsafe(ref negativeBase, (nuint)x);
}
}
if (Vector256.IsHardwareAccelerated)
{
int oneVectorFromEnd = even.Length - Vector256<int>.Count;
for (; x <= oneVectorFromEnd; x += Vector256<int>.Count)
{
Vector256<int> evenValues = Vector256.LoadUnsafe(ref evenBase, (nuint)x);
Vector256<int> oddValues = Vector256.LoadUnsafe(ref oddBase, (nuint)x);
Vector256<int> added = evenValues + oddValues;
Vector256<int> subtracted = evenValues - oddValues;
if (roundAndClip)
{
added = RoundShiftAndClamp(added, shift, minimum, maximum);
subtracted = RoundShiftAndClamp(subtracted, shift, minimum, maximum);
}
added.StoreUnsafe(ref positiveBase, (nuint)x);
subtracted.StoreUnsafe(ref negativeBase, (nuint)x);
}
}
if (Vector128.IsHardwareAccelerated)
{
int oneVectorFromEnd = even.Length - Vector128<int>.Count;
for (; x <= oneVectorFromEnd; x += Vector128<int>.Count)
{
Vector128<int> evenValues = Vector128.LoadUnsafe(ref evenBase, (nuint)x);
Vector128<int> oddValues = Vector128.LoadUnsafe(ref oddBase, (nuint)x);
Vector128<int> added = evenValues + oddValues;
Vector128<int> subtracted = evenValues - oddValues;
if (roundAndClip)
{
added = RoundShiftAndClamp(added, shift, minimum, maximum);
subtracted = RoundShiftAndClamp(subtracted, shift, minimum, maximum);
}
added.StoreUnsafe(ref positiveBase, (nuint)x);
subtracted.StoreUnsafe(ref negativeBase, (nuint)x);
}
}
for (; x < even.Length; x++)
{
int added = even[x] + odd[x];
int subtracted = even[x] - odd[x];
if (roundAndClip)
{
added = RoundShiftAndClamp(added, shift, minimum, maximum);
subtracted = RoundShiftAndClamp(subtracted, shift, minimum, maximum);
}
positive[x] = added;
negative[x] = subtracted;
}
}
/// <summary>
/// Applies the four-point inverse-DST matrix across all independent lines.
/// </summary>
/// <typeparam name="TOperator">The selected dense transform operator.</typeparam>
/// <param name="source">The frequency rows followed by contiguous independent lines.</param>
/// <param name="destination">The spatial rows followed by contiguous independent lines.</param>
/// <param name="lineCount">The number of independent lines transformed together.</param>
/// <param name="shift">The rounded right shift applied to each result.</param>
/// <param name="minimum">The inclusive output minimum.</param>
/// <param name="maximum">The inclusive output maximum.</param>
private static void TransformDense<TOperator>(
ReadOnlySpan<int> source,
Span<int> destination,
int lineCount,
int shift,
int minimum,
int maximum)
where TOperator : struct, IHevcInverseTransformOperator
{
ref int sourceBase = ref MemoryMarshal.GetReference(source);
ref int destinationBase = ref MemoryMarshal.GetReference(destination);
for (int position = 0; position < TOperator.Size; position++)
{
int x = 0;
if (Vector512.IsHardwareAccelerated)
{
int oneVectorFromEnd = lineCount - Vector512<int>.Count;
for (; x <= oneVectorFromEnd; x += Vector512<int>.Count)
{
Vector512<int> sum = Vector512<int>.Zero;
for (int frequency = 0; frequency < TOperator.Size; frequency++)
{
Vector512<int> values = Vector512.LoadUnsafe(ref sourceBase, (nuint)((frequency * lineCount) + x));
sum += values * Vector512.Create(TOperator.GetCoefficient(frequency, position));
}
RoundShiftAndClamp(sum, shift, minimum, maximum).StoreUnsafe(ref destinationBase, (nuint)((position * lineCount) + x));
}
}
if (Vector256.IsHardwareAccelerated)
{
int oneVectorFromEnd = lineCount - Vector256<int>.Count;
for (; x <= oneVectorFromEnd; x += Vector256<int>.Count)
{
Vector256<int> sum = Vector256<int>.Zero;
for (int frequency = 0; frequency < TOperator.Size; frequency++)
{
Vector256<int> values = Vector256.LoadUnsafe(ref sourceBase, (nuint)((frequency * lineCount) + x));
sum += values * Vector256.Create(TOperator.GetCoefficient(frequency, position));
}
RoundShiftAndClamp(sum, shift, minimum, maximum).StoreUnsafe(ref destinationBase, (nuint)((position * lineCount) + x));
}
}
if (Vector128.IsHardwareAccelerated)
{
int oneVectorFromEnd = lineCount - Vector128<int>.Count;
for (; x <= oneVectorFromEnd; x += Vector128<int>.Count)
{
Vector128<int> sum = Vector128<int>.Zero;
for (int frequency = 0; frequency < TOperator.Size; frequency++)
{
Vector128<int> values = Vector128.LoadUnsafe(ref sourceBase, (nuint)((frequency * lineCount) + x));
sum += values * Vector128.Create(TOperator.GetCoefficient(frequency, position));
}
RoundShiftAndClamp(sum, shift, minimum, maximum).StoreUnsafe(ref destinationBase, (nuint)((position * lineCount) + x));
}
}
for (; x < lineCount; x++)
{
int sum = 0;
for (int frequency = 0; frequency < TOperator.Size; frequency++)
{
sum += source[(frequency * lineCount) + x] * TOperator.GetCoefficient(frequency, position);
}
destination[(position * lineCount) + x] = RoundShiftAndClamp(sum, shift, minimum, maximum);
}
}
}
/// <summary>
/// Transposes one rectangular transform block into a separate full-block buffer.
/// </summary>
/// <param name="source">The source block in raster order.</param>
/// <param name="destination">The transposed destination block.</param>
/// <param name="sourceHeight">The source row count.</param>
/// <param name="sourceWidth">The source column count.</param>
private static void Transpose(ReadOnlySpan<int> source, Span<int> destination, int sourceHeight, int sourceWidth)
{
if (Vector128.IsHardwareAccelerated)
{
ref int sourceBase = ref MemoryMarshal.GetReference(source);
ref int destinationBase = ref MemoryMarshal.GetReference(destination);
// Every supported HEVC transform dimension is a multiple of four. Complete four-by-four tiles therefore
// transpose the rectangular block without masked loads, partial stores, or access to row padding.
for (int y = 0; y < sourceHeight; y += 4)
{
for (int x = 0; x < sourceWidth; x += 4)
{
Vector128<int> row0 = Vector128.LoadUnsafe(ref sourceBase, (nuint)((y * sourceWidth) + x));
Vector128<int> row1 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 1) * sourceWidth) + x));
Vector128<int> row2 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 2) * sourceWidth) + x));
Vector128<int> row3 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 3) * sourceWidth) + x));
Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3);
row0.StoreUnsafe(ref destinationBase, (nuint)((x * sourceHeight) + y));
row1.StoreUnsafe(ref destinationBase, (nuint)(((x + 1) * sourceHeight) + y));
row2.StoreUnsafe(ref destinationBase, (nuint)(((x + 2) * sourceHeight) + y));
row3.StoreUnsafe(ref destinationBase, (nuint)(((x + 3) * sourceHeight) + y));
}
}
return;
}
for (int y = 0; y < sourceHeight; y++)
{
for (int x = 0; x < sourceWidth; x++)
{
destination[(x * sourceHeight) + y] = source[(y * sourceWidth) + x];
}
}
}
/// <summary>
/// Adds a complete signed residual block to the predicted samples and clips to the component precision.
/// </summary>
/// <param name="residual">The signed residual block in raster order.</param>
/// <param name="destination">The predicted samples beginning at the block origin.</param>
/// <param name="destinationStride">The destination row stride in samples.</param>
/// <param name="width">The transform-block width.</param>
/// <param name="height">The transform-block height.</param>
/// <param name="bitDepth">The reconstructed component precision.</param>
public static void AddResidual(ReadOnlySpan<int> residual, Span<ushort> destination, int destinationStride, int width, int height, int bitDepth)
{
int maximum = (1 << bitDepth) - 1;
for (int y = 0; y < height; y++)
{
ReadOnlySpan<int> residualRow = residual.Slice(y * width, width);
Span<ushort> destinationRow = destination.Slice(y * destinationStride, width);
ref int residualBase = ref MemoryMarshal.GetReference(residualRow);
ref ushort destinationBase = ref MemoryMarshal.GetReference(destinationRow);
int x = 0;
if (Vector256.IsHardwareAccelerated)
{
// Eight UInt16 predictions widen into one Int32 vector so residual addition cannot overflow sample
// storage. Narrowing occurs only after clipping and stores exactly the eight logical destination values.
int oneVectorFromEnd = width - Vector256<int>.Count;
for (; x <= oneVectorFromEnd; x += Vector256<int>.Count)
{
Vector128<ushort> predicted16 = Vector128.LoadUnsafe(ref destinationBase, (nuint)x);
Vector256<int> predicted = Vector256.Create(Vector128.WidenLower(predicted16), Vector128.WidenUpper(predicted16)).AsInt32();
Vector256<int> reconstructed = Vector256.Clamp(
predicted + Vector256.LoadUnsafe(ref residualBase, (nuint)x),
Vector256<int>.Zero,
Vector256.Create(maximum));
Vector128.Narrow(reconstructed.GetLower().AsUInt32(), reconstructed.GetUpper().AsUInt32()).StoreUnsafe(ref destinationBase, (nuint)x);
}
}
if (Vector128.IsHardwareAccelerated)
{
// The four-sample path uses exact 64-bit loads and stores; it does not depend on writable row padding.
int oneVectorFromEnd = width - Vector128<int>.Count;
for (; x <= oneVectorFromEnd; x += Vector128<int>.Count)
{
ulong packed = Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<ushort, byte>(ref Unsafe.Add(ref destinationBase, x)));
Vector128<int> predicted = Vector128.WidenLower(Vector128.CreateScalar(packed).AsUInt16()).AsInt32();
Vector128<int> reconstructed = Vector128.Clamp(
predicted + Vector128.LoadUnsafe(ref residualBase, (nuint)x),
Vector128<int>.Zero,
Vector128.Create(maximum));
Vector128<ushort> narrowed = Vector128.Narrow(reconstructed.AsUInt32(), Vector128<uint>.Zero);
Unsafe.WriteUnaligned(ref Unsafe.As<ushort, byte>(ref Unsafe.Add(ref destinationBase, x)), narrowed.AsUInt64().ToScalar());
}
}
for (; x < width; x++)
{
destinationRow[x] = (ushort)Math.Clamp(destinationRow[x] + residualRow[x], 0, maximum);
}
}
}
/// <summary>
/// Applies HEVC's rounded right shift and inclusive clipping to a 512-bit vector.
/// </summary>
/// <param name="value">The unnormalized transform values.</param>
/// <param name="shift">The right-shift count.</param>
/// <param name="minimum">The inclusive result minimum.</param>
/// <param name="maximum">The inclusive result maximum.</param>
/// <returns>The normalized and clipped values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<int> RoundShiftAndClamp(Vector512<int> value, int shift, int minimum, int maximum)
=> Vector512.Clamp((value + Vector512.Create(1 << (shift - 1))) >> shift, Vector512.Create(minimum), Vector512.Create(maximum));
/// <summary>
/// Applies HEVC's rounded right shift and inclusive clipping to a 256-bit vector.
/// </summary>
/// <param name="value">The unnormalized transform values.</param>
/// <param name="shift">The right-shift count.</param>
/// <param name="minimum">The inclusive result minimum.</param>
/// <param name="maximum">The inclusive result maximum.</param>
/// <returns>The normalized and clipped values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<int> RoundShiftAndClamp(Vector256<int> value, int shift, int minimum, int maximum)
=> Vector256.Clamp((value + Vector256.Create(1 << (shift - 1))) >> shift, Vector256.Create(minimum), Vector256.Create(maximum));
/// <summary>
/// Applies HEVC's rounded right shift and inclusive clipping to a 128-bit vector.
/// </summary>
/// <param name="value">The unnormalized transform values.</param>
/// <param name="shift">The right-shift count.</param>
/// <param name="minimum">The inclusive result minimum.</param>
/// <param name="maximum">The inclusive result maximum.</param>
/// <returns>The normalized and clipped values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<int> RoundShiftAndClamp(Vector128<int> value, int shift, int minimum, int maximum)
=> Vector128.Clamp((value + Vector128.Create(1 << (shift - 1))) >> shift, Vector128.Create(minimum), Vector128.Create(maximum));
/// <summary>
/// Applies HEVC's rounded right shift and inclusive clipping to one scalar value.
/// </summary>
/// <param name="value">The unnormalized transform value.</param>
/// <param name="shift">The right-shift count.</param>
/// <param name="minimum">The inclusive result minimum.</param>
/// <param name="maximum">The inclusive result maximum.</param>
/// <returns>The normalized and clipped value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int RoundShiftAndClamp(int value, int shift, int minimum, int maximum)
=> Math.Clamp((value + (1 << (shift - 1))) >> shift, minimum, maximum);
}

34
src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.Operator.cs

@ -1,34 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Defines the HEVC inverse-transform operator contract.
/// </content>
internal static partial class HevcInverseTransformer
{
/// <summary>
/// Defines one closed inverse-transform operation selected by the transform-unit syntax.
/// </summary>
private interface IHevcInverseTransformOperator
{
/// <summary>
/// Gets the transform side in samples.
/// </summary>
public static abstract int Size { get; }
/// <summary>
/// Gets a value indicating whether the transform uses the partial-butterfly factorization.
/// </summary>
public static abstract bool UsesButterfly { get; }
/// <summary>
/// Gets one inverse-transform matrix coefficient.
/// </summary>
/// <param name="frequency">The frequency-domain coordinate.</param>
/// <param name="position">The spatial-domain coordinate.</param>
/// <returns>The signed transform coefficient.</returns>
public static abstract int GetCoefficient(int frequency, int position);
}
}

244
src/ImageSharp/Formats/Heif/Hevc/HevcInverseTransformer.cs

@ -1,244 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Common.Helpers;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Applies HEVC inverse transforms and reconstructs predicted samples.
/// </summary>
internal static partial class HevcInverseTransformer
{
/// <summary>
/// The signed residual precision used after the second inverse-transform pass.
/// </summary>
private const int ResidualPrecision = 16;
/// <summary>
/// Gets the scratch length required for the specified rectangular transform block.
/// </summary>
/// <param name="log2Width">The base-two logarithm of the transform-block width.</param>
/// <param name="log2Height">The base-two logarithm of the transform-block height.</param>
/// <returns>The required number of signed thirty-two-bit elements.</returns>
public static int GetScratchLength(int log2Width, int log2Height)
{
DebugGuard.MustBeBetweenOrEqualTo(log2Width, 2, 5, nameof(log2Width));
DebugGuard.MustBeBetweenOrEqualTo(log2Height, 2, 5, nameof(log2Height));
return 2 << (log2Width + log2Height);
}
/// <summary>
/// Reconstructs one transform block by adding its inverse-transformed residual to the predicted samples.
/// </summary>
/// <param name="coefficients">The dequantized transform coefficients in raster order.</param>
/// <param name="destination">The predicted samples beginning at the transform-block origin.</param>
/// <param name="destinationStride">The destination row stride in samples.</param>
/// <param name="log2Width">The base-two logarithm of the transform-block width.</param>
/// <param name="log2Height">The base-two logarithm of the transform-block height.</param>
/// <param name="bitDepth">The reconstructed component precision.</param>
/// <param name="maxTransformDynamicRange">The transform dynamic range excluding its sign bit.</param>
/// <param name="useDiscreteSineTransform">Whether the four-by-four luma intra block uses the discrete sine transform.</param>
/// <param name="scratch">The caller-owned scratch returned by <see cref="GetScratchLength(int, int)"/>.</param>
public static void TransformAdd(
ReadOnlySpan<int> coefficients,
Span<ushort> destination,
int destinationStride,
int log2Width,
int log2Height,
int bitDepth,
int maxTransformDynamicRange,
bool useDiscreteSineTransform,
Span<int> scratch)
{
DebugGuard.MustBeBetweenOrEqualTo(log2Width, 2, 5, nameof(log2Width));
DebugGuard.MustBeBetweenOrEqualTo(log2Height, 2, 5, nameof(log2Height));
DebugGuard.MustBeBetweenOrEqualTo(bitDepth, 8, 16, nameof(bitDepth));
DebugGuard.IsTrue(!useDiscreteSineTransform || (log2Width == 2 && log2Height == 2), "The HEVC inverse DST is defined only for four-by-four blocks.");
int width = 1 << log2Width;
int height = 1 << log2Height;
int sampleCount = width * height;
DebugGuard.IsTrue(coefficients.Length >= sampleCount, "The coefficient span is shorter than the transform block.");
DebugGuard.IsTrue(scratch.Length >= sampleCount * 2, "The scratch span is shorter than the inverse-transform requirement.");
Span<int> first = scratch[..sampleCount];
Span<int> second = scratch.Slice(sampleCount, sampleCount);
Span<int> residual = TransformCore(
coefficients[..sampleCount],
first,
second,
width,
height,
bitDepth,
maxTransformDynamicRange,
useDiscreteSineTransform);
AddResidual(residual, destination, destinationStride, width, height, bitDepth);
}
/// <summary>
/// Applies one two-dimensional inverse transform and writes signed residual samples in raster order.
/// </summary>
/// <param name="coefficients">The dequantized transform coefficients in raster order.</param>
/// <param name="residual">The destination residual samples in raster order.</param>
/// <param name="log2Width">The base-two logarithm of the transform-block width.</param>
/// <param name="log2Height">The base-two logarithm of the transform-block height.</param>
/// <param name="bitDepth">The reconstructed component precision.</param>
/// <param name="maxTransformDynamicRange">The transform dynamic range excluding its sign bit.</param>
/// <param name="useDiscreteSineTransform">Whether the four-by-four luma intra block uses the discrete sine transform.</param>
/// <param name="scratch">The caller-owned scratch returned by <see cref="GetScratchLength(int, int)"/>.</param>
public static void Transform(
ReadOnlySpan<int> coefficients,
Span<int> residual,
int log2Width,
int log2Height,
int bitDepth,
int maxTransformDynamicRange,
bool useDiscreteSineTransform,
Span<int> scratch)
{
int width = 1 << log2Width;
int height = 1 << log2Height;
int sampleCount = width * height;
DebugGuard.IsTrue(residual.Length >= sampleCount, "The residual span is shorter than the transform block.");
DebugGuard.IsTrue(scratch.Length >= sampleCount * 2, "The scratch span is shorter than the inverse-transform requirement.");
Span<int> transformed = TransformCore(
coefficients[..sampleCount],
scratch[..sampleCount],
scratch.Slice(sampleCount, sampleCount),
width,
height,
bitDepth,
maxTransformDynamicRange,
useDiscreteSineTransform);
transformed.CopyTo(residual);
}
/// <summary>
/// Dispatches both separable transform passes through closed operators selected from the block dimensions.
/// </summary>
/// <param name="coefficients">The complete dequantized coefficient block.</param>
/// <param name="first">The first full-block scratch buffer.</param>
/// <param name="second">The second full-block scratch buffer.</param>
/// <param name="width">The transform-block width.</param>
/// <param name="height">The transform-block height.</param>
/// <param name="bitDepth">The reconstructed component precision.</param>
/// <param name="maxTransformDynamicRange">The transform dynamic range excluding its sign bit.</param>
/// <param name="useDiscreteSineTransform">Whether both four-point passes use the discrete sine transform.</param>
/// <returns>The scratch buffer containing the raster-ordered residual.</returns>
private static Span<int> TransformCore(
ReadOnlySpan<int> coefficients,
Span<int> first,
Span<int> second,
int width,
int height,
int bitDepth,
int maxTransformDynamicRange,
bool useDiscreteSineTransform)
{
int dynamicMinimum = -(1 << maxTransformDynamicRange);
int dynamicMaximum = (1 << maxTransformDynamicRange) - 1;
// HEVC moves one normalization bit from the second pass to the first. The intermediate clip therefore
// belongs after the vertical pass and must not be combined with the final residual clipping operation.
Span<int> vertical = TransformDimension(
coefficients,
first,
second,
height,
width,
7,
dynamicMinimum,
dynamicMaximum,
useDiscreteSineTransform);
Span<int> horizontalInput = vertical.Overlaps(first) ? second : first;
Transpose(vertical, horizontalInput, height, width);
Span<int> horizontalWorkspace = horizontalInput.Overlaps(first) ? second : first;
int secondShift = maxTransformDynamicRange + 5 - bitDepth;
Span<int> horizontal = TransformDimension(
horizontalInput,
horizontalWorkspace,
horizontalInput,
width,
height,
secondShift,
-(1 << (ResidualPrecision - 1)),
(1 << (ResidualPrecision - 1)) - 1,
useDiscreteSineTransform);
Span<int> residual = horizontal.Overlaps(first) ? second : first;
Transpose(horizontal, residual, width, height);
return residual;
}
/// <summary>
/// Selects the statically specialized operator for one transform dimension.
/// </summary>
/// <param name="source">The frequency rows followed by contiguous independent lines.</param>
/// <param name="initial">The initial operator output buffer.</param>
/// <param name="alternate">The alternate combination buffer.</param>
/// <param name="size">The transform dimension.</param>
/// <param name="lineCount">The number of independent lines transformed together.</param>
/// <param name="shift">The rounded right shift applied to the spatial results.</param>
/// <param name="minimum">The inclusive output minimum.</param>
/// <param name="maximum">The inclusive output maximum.</param>
/// <param name="useDiscreteSineTransform">Whether the four-point pass uses the discrete sine transform.</param>
/// <returns>The buffer containing spatial rows followed by contiguous independent lines.</returns>
private static Span<int> TransformDimension(
ReadOnlySpan<int> source,
Span<int> initial,
Span<int> alternate,
int size,
int lineCount,
int shift,
int minimum,
int maximum,
bool useDiscreteSineTransform)
=> (size, useDiscreteSineTransform) switch
{
(4, true) => TransformDimension<DiscreteSine4Operator>(source, initial, alternate, lineCount, shift, minimum, maximum),
(4, false) => TransformDimension<DiscreteCosine4Operator>(source, initial, alternate, lineCount, shift, minimum, maximum),
(8, _) => TransformDimension<DiscreteCosine8Operator>(source, initial, alternate, lineCount, shift, minimum, maximum),
(16, _) => TransformDimension<DiscreteCosine16Operator>(source, initial, alternate, lineCount, shift, minimum, maximum),
_ => TransformDimension<DiscreteCosine32Operator>(source, initial, alternate, lineCount, shift, minimum, maximum)
};
/// <summary>
/// Invokes one statically selected inverse-transform operator.
/// </summary>
/// <typeparam name="TOperator">The selected inverse-transform operator.</typeparam>
/// <param name="source">The frequency rows followed by contiguous independent lines.</param>
/// <param name="initial">The initial operator output buffer.</param>
/// <param name="alternate">The alternate combination buffer.</param>
/// <param name="lineCount">The number of independent lines transformed together.</param>
/// <param name="shift">The rounded right shift applied to the spatial results.</param>
/// <param name="minimum">The inclusive output minimum.</param>
/// <param name="maximum">The inclusive output maximum.</param>
/// <returns>The buffer containing spatial rows followed by contiguous independent lines.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Span<int> TransformDimension<TOperator>(
ReadOnlySpan<int> source,
Span<int> initial,
Span<int> alternate,
int lineCount,
int shift,
int minimum,
int maximum)
where TOperator : struct, IHevcInverseTransformOperator
{
if (!TOperator.UsesButterfly)
{
TransformDense<TOperator>(source, initial, lineCount, shift, minimum, maximum);
return initial;
}
PopulateButterflyGroups<TOperator>(source, initial, lineCount);
return CombineButterflyGroups<TOperator>(initial, alternate, lineCount, shift, minimum, maximum);
}
}

148
src/ImageSharp/Formats/Heif/Hevc/HevcNalUnit.cs

@ -1,148 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Contains one decoded HEVC network abstraction layer unit.
/// </summary>
internal sealed class HevcNalUnit
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcNalUnit"/> class.
/// </summary>
/// <param name="data">The complete NAL unit, including its two-byte header.</param>
/// <exception cref="InvalidImageContentException">The NAL header or encoded payload is malformed.</exception>
public HevcNalUnit(ReadOnlySpan<byte> data)
{
this.Header = HevcNalUnitHeader.Parse(data);
// Container and configuration NAL units carry EBSP bytes. Decode them once at the boundary so every
// parameter-set and slice parser observes the same validated RBSP representation.
ReadOnlySpan<byte> encodedPayload = data[2..];
byte[] rbspBuffer = new byte[encodedPayload.Length];
int rbspLength = HevcRbspDecoder.Decode(
encodedPayload,
rbspBuffer,
out ReadOnlyMemory<int> emulationPreventionBytePositions);
this.EncodedPayloadLength = encodedPayload.Length;
this.Rbsp = rbspBuffer.AsMemory(0, rbspLength);
this.EmulationPreventionBytePositions = emulationPreventionBytePositions;
}
/// <summary>
/// Gets the decoded two-byte NAL-unit header.
/// </summary>
public HevcNalUnitHeader Header { get; }
/// <summary>
/// Gets the raw byte sequence payload after removal of emulation-prevention bytes.
/// </summary>
public ReadOnlyMemory<byte> Rbsp { get; }
/// <summary>
/// Gets the encoded byte-sequence payload length before removal of emulation-prevention bytes.
/// </summary>
public int EncodedPayloadLength { get; }
/// <summary>
/// Gets the zero-based encoded-payload positions of removed emulation-prevention bytes.
/// </summary>
public ReadOnlyMemory<int> EmulationPreventionBytePositions { get; }
}
/// <summary>
/// Removes HEVC emulation-prevention bytes from an encoded raw byte sequence payload.
/// </summary>
internal static class HevcRbspDecoder
{
/// <summary>
/// Decodes an encoded byte sequence payload into a raw byte sequence payload.
/// </summary>
/// <param name="encodedPayload">The NAL payload following the two-byte header.</param>
/// <param name="destination">A buffer at least as long as <paramref name="encodedPayload"/>.</param>
/// <param name="emulationPreventionBytePositions">
/// Receives the zero-based encoded-payload positions of removed emulation-prevention bytes.
/// </param>
/// <returns>The number of decoded bytes written to <paramref name="destination"/>.</returns>
/// <exception cref="InvalidImageContentException">
/// The payload contains a forbidden start-code-like byte sequence or an invalid emulation-prevention byte.
/// </exception>
public static int Decode(
ReadOnlySpan<byte> encodedPayload,
Span<byte> destination,
out ReadOnlyMemory<int> emulationPreventionBytePositions)
{
DebugGuard.MustBeGreaterThanOrEqualTo(destination.Length, encodedPayload.Length, nameof(destination));
int destinationOffset = 0;
int preventionByteCount = 0;
int consecutiveZeroBytes = 0;
int[]? rentedPositions = null;
Span<int> preventionBytePositions = [];
try
{
for (int sourceOffset = 0; sourceOffset < encodedPayload.Length; sourceOffset++)
{
byte value = encodedPayload[sourceOffset];
// HEVC section 7.3.1.1 forbids 00 00 00 through 00 00 02 in EBSP form. A 03 after two zeros is an
// emulation-prevention byte only when another byte in the range 00 through 03 follows it.
if (consecutiveZeroBytes == 2)
{
if (value < 3)
{
throw new InvalidImageContentException("The HEVC NAL unit contains a forbidden start-code-like byte sequence.");
}
if (value == 3)
{
sourceOffset++;
if (sourceOffset == encodedPayload.Length || encodedPayload[sourceOffset] > 3)
{
throw new InvalidImageContentException("The HEVC NAL unit contains an invalid emulation-prevention byte.");
}
if (preventionByteCount == preventionBytePositions.Length)
{
int[] expandedPositions = ArrayPool<int>.Shared.Rent(preventionBytePositions.IsEmpty ? 16 : preventionBytePositions.Length * 2);
preventionBytePositions.CopyTo(expandedPositions);
if (rentedPositions is not null)
{
ArrayPool<int>.Shared.Return(rentedPositions);
}
rentedPositions = expandedPositions;
preventionBytePositions = rentedPositions;
}
preventionBytePositions[preventionByteCount++] = sourceOffset - 1;
value = encodedPayload[sourceOffset];
consecutiveZeroBytes = 0;
}
}
destination[destinationOffset++] = value;
consecutiveZeroBytes = value == 0 ? consecutiveZeroBytes + 1 : 0;
}
int[] retainedPositions = preventionByteCount == 0
? []
: GC.AllocateUninitializedArray<int>(preventionByteCount);
preventionBytePositions[..preventionByteCount].CopyTo(retainedPositions);
emulationPreventionBytePositions = retainedPositions;
return destinationOffset;
}
finally
{
if (rentedPositions is not null)
{
ArrayPool<int>.Shared.Return(rentedPositions);
}
}
}
}

78
src/ImageSharp/Formats/Heif/Hevc/HevcNalUnitHeader.cs

@ -1,78 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Contains the type, layer, and temporal identifier encoded by an HEVC NAL-unit header.
/// </summary>
internal readonly struct HevcNalUnitHeader
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcNalUnitHeader"/> struct.
/// </summary>
/// <param name="nalUnitType">The six-bit NAL-unit type.</param>
/// <param name="layerId">The six-bit layer identifier.</param>
/// <param name="temporalId">The zero-based temporal identifier.</param>
private HevcNalUnitHeader(byte nalUnitType, byte layerId, byte temporalId)
{
this.NalUnitType = nalUnitType;
this.LayerId = layerId;
this.TemporalId = temporalId;
}
/// <summary>
/// Gets the six-bit NAL-unit type.
/// </summary>
public byte NalUnitType { get; }
/// <summary>
/// Gets the six-bit layer identifier.
/// </summary>
public byte LayerId { get; }
/// <summary>
/// Gets the zero-based temporal identifier.
/// </summary>
public byte TemporalId { get; }
/// <summary>
/// Gets a value indicating whether the NAL unit contains coded slice-segment data.
/// </summary>
public bool IsVideoCodingLayer => this.NalUnitType <= 31;
/// <summary>
/// Gets a value indicating whether the NAL unit begins an instantaneous decoder refresh picture.
/// </summary>
public bool IsInstantaneousDecoderRefresh => this.NalUnitType is 19 or 20;
/// <summary>
/// Reads and validates an HEVC NAL-unit header.
/// </summary>
/// <param name="data">The complete NAL unit beginning with its two-byte header.</param>
/// <returns>The decoded header.</returns>
/// <exception cref="InvalidImageContentException">
/// The header is truncated, its forbidden bit is set, or its temporal identifier is reserved.
/// </exception>
public static HevcNalUnitHeader Parse(ReadOnlySpan<byte> data)
{
if (data.Length < 2)
{
throw new InvalidImageContentException("The HEVC NAL-unit header is truncated.");
}
// Use the same bounded MSB-first reader as the RBSP parsers so header truncation and field ordering have
// one behavior model instead of a second set of shifts and masks.
HevcBitReader reader = new(data[..2]);
bool forbiddenZeroBit = reader.ReadFlag();
byte nalUnitType = (byte)reader.ReadBits(6);
byte layerId = (byte)reader.ReadBits(6);
byte temporalIdPlusOne = (byte)reader.ReadBits(3);
if (forbiddenZeroBit || temporalIdPlusOne == 0)
{
throw new InvalidImageContentException("The HEVC NAL-unit header is invalid.");
}
return new HevcNalUnitHeader(nalUnitType, layerId, (byte)(temporalIdPlusOne - 1));
}
}

186
src/ImageSharp/Formats/Heif/Hevc/HevcParameterSetSyntax.cs

@ -1,186 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Provides shared bounded syntax operations used by HEVC parameter-set readers.
/// </summary>
internal static class HevcParameterSetSyntax
{
/// <summary>
/// Gets the horizontal conformance-window unit for an HEVC chroma layout.
/// </summary>
/// <param name="chromaFormat">The chroma-format identifier.</param>
/// <param name="separateColorPlane">Whether 4:4:4 components are coded as separate color planes.</param>
/// <returns>The horizontal unit in luma samples.</returns>
public static int GetCropUnitWidth(byte chromaFormat, bool separateColorPlane)
=> !separateColorPlane && chromaFormat is 1 or 2 ? 2 : 1;
/// <summary>
/// Gets the vertical conformance-window unit for an HEVC chroma layout.
/// </summary>
/// <param name="chromaFormat">The chroma-format identifier.</param>
/// <param name="separateColorPlane">Whether 4:4:4 components are coded as separate color planes.</param>
/// <returns>The vertical unit in luma samples.</returns>
public static int GetCropUnitHeight(byte chromaFormat, bool separateColorPlane)
=> !separateColorPlane && chromaFormat == 1 ? 2 : 1;
/// <summary>
/// Gets the number of coding-tree blocks needed to cover one coded picture dimension.
/// </summary>
/// <param name="sampleCount">The coded luma-sample count.</param>
/// <param name="codingTreeBlockLog2">The base-two logarithm of the coding-tree-block size.</param>
/// <returns>The covering coding-tree-block count.</returns>
public static int GetCodingTreeBlockCount(int sampleCount, int codingTreeBlockLog2)
=> ((sampleCount - 1) >> codingTreeBlockLog2) + 1;
/// <summary>
/// Gets the number of bits required to represent values below a positive exclusive upper bound.
/// </summary>
/// <param name="exclusiveUpperBound">The positive exclusive upper bound.</param>
/// <returns>The ceiling of the base-two logarithm, with zero returned for an upper bound of one.</returns>
public static int GetCeilingLog2(int exclusiveUpperBound)
{
DebugGuard.MustBeGreaterThan(exclusiveUpperBound, 0, nameof(exclusiveUpperBound));
int bitCount = 0;
int remaining = exclusiveUpperBound - 1;
while (remaining > 0)
{
bitCount++;
remaining >>= 1;
}
return bitCount;
}
/// <summary>
/// Reads a signed chroma quantization-parameter offset.
/// </summary>
/// <param name="reader">The HEVC syntax reader.</param>
/// <returns>The decoded offset in the registered range from negative twelve through twelve.</returns>
/// <exception cref="InvalidImageContentException">The offset is outside its registered range.</exception>
public static int ReadQuantizationParameterOffset(ref HevcBitReader reader)
{
int offset = reader.ReadSignedExpGolomb();
if (offset is < -12 or > 12)
{
throw new InvalidImageContentException("The HEVC chroma quantization-parameter offset is invalid.");
}
return offset;
}
/// <summary>
/// Reads a signed deblocking-filter threshold offset.
/// </summary>
/// <param name="reader">The HEVC syntax reader.</param>
/// <returns>The decoded half-offset in the registered range from negative six through six.</returns>
/// <exception cref="InvalidImageContentException">The offset is outside its registered range.</exception>
public static int ReadDeblockingFilterOffset(ref HevcBitReader reader)
{
int offset = reader.ReadSignedExpGolomb();
if (offset is < -6 or > 6)
{
throw new InvalidImageContentException("The HEVC deblocking-filter offset is invalid.");
}
return offset;
}
/// <summary>
/// Consumes hypothetical-reference-decoder syntax without adding playback state to the still-image model.
/// </summary>
/// <param name="reader">The parameter-set raw byte sequence payload reader.</param>
/// <param name="commonInformationPresent">Whether common HRD flags are coded for this parameter set.</param>
/// <param name="maxSubLayersMinusOne">The highest declared temporal sublayer index.</param>
/// <param name="nalHrdParametersPresent">The effective NAL HRD presence flag.</param>
/// <param name="vclHrdParametersPresent">The effective VCL HRD presence flag.</param>
/// <param name="subPictureHrdParametersPresent">The effective sub-picture HRD presence flag.</param>
/// <exception cref="InvalidImageContentException">The HRD syntax is truncated or exceeds its registered bounds.</exception>
public static void SkipHrdParameters(
ref HevcBitReader reader,
bool commonInformationPresent,
int maxSubLayersMinusOne,
ref bool nalHrdParametersPresent,
ref bool vclHrdParametersPresent,
ref bool subPictureHrdParametersPresent)
{
if (commonInformationPresent)
{
nalHrdParametersPresent = reader.ReadFlag();
vclHrdParametersPresent = reader.ReadFlag();
subPictureHrdParametersPresent = false;
if (nalHrdParametersPresent || vclHrdParametersPresent)
{
subPictureHrdParametersPresent = reader.ReadFlag();
if (subPictureHrdParametersPresent)
{
reader.ReadBits(8);
reader.ReadBits(5);
reader.ReadFlag();
reader.ReadBits(5);
}
reader.ReadBits(4);
reader.ReadBits(4);
if (subPictureHrdParametersPresent)
{
reader.ReadBits(4);
}
reader.ReadBits(5);
reader.ReadBits(5);
reader.ReadBits(5);
}
}
for (int subLayer = 0; subLayer <= maxSubLayersMinusOne; subLayer++)
{
bool fixedPictureRateGeneral = reader.ReadFlag();
bool fixedPictureRateWithinCvs = fixedPictureRateGeneral || reader.ReadFlag();
bool lowDelayHrd = false;
if (fixedPictureRateWithinCvs)
{
reader.ReadUnsignedExpGolomb();
}
else
{
lowDelayHrd = reader.ReadFlag();
}
uint cpbCountMinusOne = 0;
if (!lowDelayHrd)
{
cpbCountMinusOne = reader.ReadUnsignedExpGolomb();
if (cpbCountMinusOne > 31)
{
throw new InvalidImageContentException("The HEVC HRD syntax declares too many coded-picture buffers.");
}
}
for (int hrdKind = 0; hrdKind < 2; hrdKind++)
{
bool parametersPresent = hrdKind == 0 ? nalHrdParametersPresent : vclHrdParametersPresent;
if (!parametersPresent)
{
continue;
}
for (uint cpbIndex = 0; cpbIndex <= cpbCountMinusOne; cpbIndex++)
{
reader.ReadUnsignedExpGolomb();
reader.ReadUnsignedExpGolomb();
if (subPictureHrdParametersPresent)
{
reader.ReadUnsignedExpGolomb();
reader.ReadUnsignedExpGolomb();
}
reader.ReadFlag();
}
}
}
}
}

235
src/ImageSharp/Formats/Heif/Hevc/HevcPictureBuffer.cs

@ -1,235 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Owns the native-precision luma and chroma sample planes for one reconstructed HEVC still picture.
/// </summary>
internal sealed class HevcPictureBuffer : IDisposable
{
/// <summary>
/// The horizontal chroma subsampling shift.
/// </summary>
private readonly int chromaSubsamplingX;
/// <summary>
/// The vertical chroma subsampling shift.
/// </summary>
private readonly int chromaSubsamplingY;
/// <summary>
/// Initializes a new instance of the <see cref="HevcPictureBuffer"/> class.
/// </summary>
/// <param name="configuration">The configuration providing the image memory allocator.</param>
/// <param name="sequenceParameterSet">The coded dimensions, precision, and chroma layout.</param>
public HevcPictureBuffer(Configuration configuration, HevcSequenceParameterSet sequenceParameterSet)
: this(
configuration,
sequenceParameterSet.Width,
sequenceParameterSet.Height,
sequenceParameterSet.BitDepthLuma,
sequenceParameterSet.BitDepthChroma,
sequenceParameterSet.ChromaFormat,
sequenceParameterSet.SeparateColorPlaneFlag,
1 << sequenceParameterSet.MinCodingBlockLog2)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="HevcPictureBuffer"/> class for encoder-owned component planes.
/// </summary>
/// <param name="configuration">The configuration providing the image memory allocator.</param>
/// <param name="width">The coded luma width.</param>
/// <param name="height">The coded luma height.</param>
/// <param name="bitDepthLuma">The luma sample precision.</param>
/// <param name="bitDepthChroma">The chroma sample precision.</param>
/// <param name="chromaFormat">The HEVC chroma-format identifier.</param>
/// <param name="separateColorPlane">Whether 4:4:4 components are coded as separate color planes.</param>
/// <param name="storageAlignment">The luma sample alignment applied to the owned reconstruction planes.</param>
public HevcPictureBuffer(
Configuration configuration,
int width,
int height,
int bitDepthLuma,
int bitDepthChroma,
byte chromaFormat,
bool separateColorPlane,
int storageAlignment = 1)
{
this.Width = width;
this.Height = height;
this.BitDepthLuma = bitDepthLuma;
this.BitDepthChroma = bitDepthChroma;
this.ChromaFormat = chromaFormat;
this.SeparateColorPlane = separateColorPlane;
// Separate color planes are independently coded at full resolution even though chroma_format_idc is 4:4:4.
this.chromaSubsamplingX = !this.SeparateColorPlane && this.ChromaFormat is 1 or 2 ? 1 : 0;
this.chromaSubsamplingY = !this.SeparateColorPlane && this.ChromaFormat == 1 ? 1 : 0;
int storageWidth = DivideCeilingByPowerOfTwo(this.Width, BitOperations.Log2((uint)storageAlignment)) * storageAlignment;
int storageHeight = DivideCeilingByPowerOfTwo(this.Height, BitOperations.Log2((uint)storageAlignment)) * storageAlignment;
Buffer2D<ushort>? luma = null;
Buffer2D<ushort>? chromaBlue = null;
Buffer2D<ushort>? chromaRed = null;
try
{
luma = configuration.MemoryAllocator.Allocate2D<ushort>(storageWidth, storageHeight);
if (this.ChromaFormat != 0)
{
int chromaWidth = DivideCeilingByPowerOfTwo(storageWidth, this.chromaSubsamplingX);
int chromaHeight = DivideCeilingByPowerOfTwo(storageHeight, this.chromaSubsamplingY);
chromaBlue = configuration.MemoryAllocator.Allocate2D<ushort>(chromaWidth, chromaHeight);
chromaRed = configuration.MemoryAllocator.Allocate2D<ushort>(chromaWidth, chromaHeight);
}
this.Luma = luma;
this.ChromaBlue = chromaBlue;
this.ChromaRed = chromaRed;
}
catch
{
// Construction transfers no plane ownership when a later rent fails, so unwind the unpublished owners
// here instead of relying on Dispose being reachable through a fully constructed picture buffer.
chromaRed?.Dispose();
chromaBlue?.Dispose();
luma?.Dispose();
throw;
}
}
/// <summary>
/// Gets the coded luma width in samples.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the coded luma height in samples.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets the luma sample precision in bits.
/// </summary>
public int BitDepthLuma { get; }
/// <summary>
/// Gets the chroma sample precision in bits.
/// </summary>
public int BitDepthChroma { get; }
/// <summary>
/// Gets the HEVC chroma-format identifier.
/// </summary>
public byte ChromaFormat { get; }
/// <summary>
/// Gets a value indicating whether the three planes are coded as independent full-resolution color planes.
/// </summary>
public bool SeparateColorPlane { get; }
/// <summary>
/// Gets the luma or first separate-color-plane allocation.
/// </summary>
public Buffer2D<ushort> Luma { get; }
/// <summary>
/// Gets the blue-difference chroma or second separate-color-plane allocation.
/// </summary>
public Buffer2D<ushort>? ChromaBlue { get; }
/// <summary>
/// Gets the red-difference chroma or third separate-color-plane allocation.
/// </summary>
public Buffer2D<ushort>? ChromaRed { get; }
/// <summary>
/// Gets the horizontal chroma subsampling shift for the selected plane.
/// </summary>
/// <param name="plane">The reconstruction plane.</param>
/// <returns>Zero for luma and full-resolution planes; otherwise, the chroma shift.</returns>
public int GetSubsamplingX(HevcPlane plane) => plane == HevcPlane.Y ? 0 : this.chromaSubsamplingX;
/// <summary>
/// Gets the vertical chroma subsampling shift for the selected plane.
/// </summary>
/// <param name="plane">The reconstruction plane.</param>
/// <returns>Zero for luma and full-resolution planes; otherwise, the chroma shift.</returns>
public int GetSubsamplingY(HevcPlane plane) => plane == HevcPlane.Y ? 0 : this.chromaSubsamplingY;
/// <summary>
/// Gets the sample precision for the selected reconstruction plane.
/// </summary>
/// <param name="plane">The reconstruction plane.</param>
/// <returns>The plane sample precision in bits.</returns>
public int GetBitDepth(HevcPlane plane) => plane == HevcPlane.Y ? this.BitDepthLuma : this.BitDepthChroma;
/// <summary>
/// Gets the selected plane width in samples.
/// </summary>
/// <param name="plane">The reconstruction plane.</param>
/// <returns>The coded plane width.</returns>
public int GetWidth(HevcPlane plane) => DivideCeilingByPowerOfTwo(this.Width, this.GetSubsamplingX(plane));
/// <summary>
/// Gets the selected plane height in samples.
/// </summary>
/// <param name="plane">The reconstruction plane.</param>
/// <returns>The coded plane height.</returns>
public int GetHeight(HevcPlane plane) => DivideCeilingByPowerOfTwo(this.Height, this.GetSubsamplingY(plane));
/// <summary>
/// Gets one coded row from the selected reconstruction plane.
/// </summary>
/// <param name="plane">The reconstruction plane.</param>
/// <param name="row">The zero-based row index in plane samples.</param>
/// <returns>The complete coded plane row.</returns>
public Span<ushort> GetRowSpan(HevcPlane plane, int row)
=> plane switch
{
HevcPlane.Y => this.Luma.DangerousGetRowSpan(row),
HevcPlane.Cb => this.ChromaBlue!.DangerousGetRowSpan(row),
_ => this.ChromaRed!.DangerousGetRowSpan(row),
};
/// <summary>
/// Copies the complete coded component planes to another picture buffer with the same dimensions and chroma layout.
/// </summary>
/// <param name="destination">The destination picture buffer.</param>
public void CopyTo(HevcPictureBuffer destination)
{
int planeCount = this.ChromaFormat == 0 ? 1 : 3;
for (int planeIndex = 0; planeIndex < planeCount; planeIndex++)
{
HevcPlane plane = (HevcPlane)planeIndex;
int width = this.GetWidth(plane);
int height = this.GetHeight(plane);
for (int row = 0; row < height; row++)
{
this.GetRowSpan(plane, row)[..width].CopyTo(destination.GetRowSpan(plane, row));
}
}
}
/// <summary>
/// Releases the owned luma and chroma plane allocations.
/// </summary>
public void Dispose()
{
this.Luma.Dispose();
this.ChromaBlue?.Dispose();
this.ChromaRed?.Dispose();
}
/// <summary>
/// Divides a nonnegative sample count by a power of two with upward rounding.
/// </summary>
/// <param name="value">The sample count.</param>
/// <param name="shift">The base-two divisor logarithm.</param>
/// <returns>The upward-rounded quotient.</returns>
private static int DivideCeilingByPowerOfTwo(int value, int shift) => (value + (1 << shift) - 1) >> shift;
}

396
src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Deblocking.cs

@ -1,396 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Implements picture-level HEVC deblocking traversal and threshold derivation.
/// </content>
internal sealed partial class HevcPictureDecoder
{
/// <summary>
/// Defines the orientation-dependent boundary lookup and four-sample filter dispatch.
/// </summary>
private interface IDeblockingDirection
{
/// <summary>
/// Gets a value indicating whether the boundary is vertical.
/// </summary>
public static abstract bool IsVertical { get; }
/// <summary>
/// Gets whether the selected four-sample segment is a transform or prediction boundary.
/// </summary>
/// <param name="state">The decoded deblocking boundary state.</param>
/// <param name="plane">The primary coding plane.</param>
/// <param name="x">The segment left luma coordinate.</param>
/// <param name="y">The segment top luma coordinate.</param>
/// <returns><see langword="true"/> when the segment is a filter candidate; otherwise, <see langword="false"/>.</returns>
public static abstract bool IsBoundary(HevcDeblockingState state, HevcPlane plane, int x, int y);
/// <summary>
/// Applies the orientation-specific luma kernel.
/// </summary>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="beta">The scaled discontinuity threshold.</param>
/// <param name="tc">The scaled clipping threshold.</param>
/// <param name="partPNoFilter">Whether the P-side block retains its original samples.</param>
/// <param name="partQNoFilter">Whether the Q-side block retains its original samples.</param>
/// <param name="bitDepth">The component sample precision.</param>
public static abstract void FilterLuma(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int beta,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth);
/// <summary>
/// Applies the orientation-specific chroma kernel.
/// </summary>
/// <param name="picture">The reconstructed picture.</param>
/// <param name="plane">The Cb or Cr component plane.</param>
/// <param name="x">The first Q-side sample X coordinate.</param>
/// <param name="y">The first Q-side sample Y coordinate.</param>
/// <param name="tc">The scaled clipping threshold.</param>
/// <param name="partPNoFilter">Whether the P-side block retains its original samples.</param>
/// <param name="partQNoFilter">Whether the Q-side block retains its original samples.</param>
/// <param name="bitDepth">The component sample precision.</param>
/// <param name="count">The number of samples in the edge segment.</param>
public static abstract void FilterChroma(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth,
int count);
}
/// <summary>
/// Gets the H.265 Table 8-20 clipping thresholds indexed by the effective boundary quantization parameter.
/// </summary>
private static ReadOnlySpan<byte> DeblockingTcTable =>
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4,
4, 4, 5, 5, 6, 6, 7, 8, 9, 10, 11, 13, 14, 16, 18, 20, 22, 24,
];
/// <summary>
/// Gets the H.265 Table 8-20 discontinuity thresholds indexed by the effective boundary quantization parameter.
/// </summary>
private static ReadOnlySpan<byte> DeblockingBetaTable =>
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 22, 24, 26,
28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64,
];
/// <summary>
/// Applies vertical edges across the complete picture before applying any horizontal edge.
/// </summary>
/// <param name="tileLayout">The picture tile mapping.</param>
private void ApplyDeblockingFilter(in HevcTileLayout tileLayout)
{
this.ApplyDeblockingDirection<VerticalDeblockingDirection>(in tileLayout);
this.ApplyDeblockingDirection<HorizontalDeblockingDirection>(in tileLayout);
}
/// <summary>
/// Applies one closed deblocking direction to every coded component plane.
/// </summary>
/// <typeparam name="TDirection">The vertical or horizontal boundary operator.</typeparam>
/// <param name="tileLayout">The picture tile mapping.</param>
private void ApplyDeblockingDirection<TDirection>(in HevcTileLayout tileLayout)
where TDirection : struct, IDeblockingDirection
{
if (this.sequenceParameterSet.SeparateColorPlaneFlag)
{
for (int planeIndex = 0; planeIndex < 3; planeIndex++)
{
this.ApplyLumaDeblocking<TDirection>((HevcPlane)planeIndex, planeIndex, in tileLayout);
}
return;
}
this.ApplyLumaDeblocking<TDirection>(HevcPlane.Y, 0, in tileLayout);
if (this.sequenceParameterSet.ChromaFormat != 0)
{
this.ApplyChromaDeblocking<TDirection>(HevcPlane.Cb, in tileLayout);
this.ApplyChromaDeblocking<TDirection>(HevcPlane.Cr, in tileLayout);
}
}
/// <summary>
/// Applies one deblocking direction with the luma kernel to a primary coded plane.
/// </summary>
/// <typeparam name="TDirection">The vertical or horizontal boundary operator.</typeparam>
/// <param name="plane">The primary coded plane.</param>
/// <param name="codingTreeStateIndex">The coding-tree state selected for the plane.</param>
/// <param name="tileLayout">The picture tile mapping.</param>
private void ApplyLumaDeblocking<TDirection>(HevcPlane plane, int codingTreeStateIndex, in HevcTileLayout tileLayout)
where TDirection : struct, IDeblockingDirection
{
int width = this.Picture.GetWidth(plane);
int height = this.Picture.GetHeight(plane);
int acrossLimit = TDirection.IsVertical ? width : height;
int alongLimit = TDirection.IsVertical ? height : width;
int bitDepth = this.Picture.GetBitDepth(plane);
int bitDepthScale = 1 << (bitDepth - 8);
int codingTreeBlockSize = 1 << this.sequenceParameterSet.CodingTreeBlockLog2;
HevcCodingTreeState codingTreeState = this.codingTreeStates[codingTreeStateIndex];
// Deblocking visits only eight-sample grid lines, but each candidate is retained at four-sample resolution
// because transform and prediction boundaries can differ between the two halves of that grid interval.
for (int edge = 8; edge < acrossLimit; edge += 8)
{
for (int along = 0; along < alongLimit; along += 4)
{
int x = TDirection.IsVertical ? edge : along;
int y = TDirection.IsVertical ? along : edge;
if (!TDirection.IsBoundary(this.deblockingState, plane, x, y))
{
continue;
}
int rasterAddress = ((y / codingTreeBlockSize) * tileLayout.Width) + (x / codingTreeBlockSize);
HevcLoopFilterRegion region = this.sampleAdaptiveOffsetState.GetLoopFilterRegion(rasterAddress, plane);
if (region.DeblockingFilterDisabled
|| !this.IsDeblockingCtbBoundaryAvailable<TDirection>(rasterAddress, plane, x, y, codingTreeBlockSize, in tileLayout))
{
continue;
}
int pX = x - (TDirection.IsVertical ? 1 : 0);
int pY = y - (TDirection.IsVertical ? 0 : 1);
int qX = x;
int qY = y;
int quantizationParameterP = codingTreeState.GetQuantizationParameter(pX, pY);
int quantizationParameterQ = codingTreeState.GetQuantizationParameter(qX, qY);
int averageQuantizationParameter = (quantizationParameterP + quantizationParameterQ + 1) >> 1;
int tcIndex = Math.Clamp(averageQuantizationParameter + 2 + (region.DeblockingFilterTcOffsetDiv2 << 1), 0, 53);
int betaIndex = Math.Clamp(averageQuantizationParameter + (region.DeblockingFilterBetaOffsetDiv2 << 1), 0, 51);
int tc = DeblockingTcTable[tcIndex] * bitDepthScale;
int beta = DeblockingBetaTable[betaIndex] * bitDepthScale;
bool partPNoFilter = this.IsDeblockingSuppressed(codingTreeState, pX, pY);
bool partQNoFilter = this.IsDeblockingSuppressed(codingTreeState, qX, qY);
TDirection.FilterLuma(this.Picture, plane, x, y, beta, tc, partPNoFilter, partQNoFilter, bitDepth);
}
}
}
/// <summary>
/// Applies one deblocking direction with the chroma kernel to a combined Cb or Cr plane.
/// </summary>
/// <typeparam name="TDirection">The vertical or horizontal boundary operator.</typeparam>
/// <param name="plane">The Cb or Cr component plane.</param>
/// <param name="tileLayout">The picture tile mapping.</param>
private void ApplyChromaDeblocking<TDirection>(HevcPlane plane, in HevcTileLayout tileLayout)
where TDirection : struct, IDeblockingDirection
{
int subsamplingX = this.Picture.GetSubsamplingX(plane);
int subsamplingY = this.Picture.GetSubsamplingY(plane);
int width = this.Picture.GetWidth(plane);
int height = this.Picture.GetHeight(plane);
int acrossLimit = TDirection.IsVertical ? width : height;
int alongLimit = TDirection.IsVertical ? height : width;
int alongSubsampling = TDirection.IsVertical ? subsamplingY : subsamplingX;
int segmentLength = 4 >> alongSubsampling;
int bitDepth = this.Picture.GetBitDepth(plane);
int bitDepthScale = 1 << (bitDepth - 8);
int codingTreeBlockSize = 1 << this.sequenceParameterSet.CodingTreeBlockLog2;
HevcCodingTreeState codingTreeState = this.codingTreeStates[0];
// Chroma deblocking uses eight-sample component-grid edges. A two-lane segment in subsampled directions still
// enters the SIMD kernel, but only its valid low lanes are committed because QP and suppression state can change next.
for (int edge = 8; edge < acrossLimit; edge += 8)
{
for (int along = 0; along < alongLimit; along += segmentLength)
{
int x = TDirection.IsVertical ? edge : along;
int y = TDirection.IsVertical ? along : edge;
int lumaX = x << subsamplingX;
int lumaY = y << subsamplingY;
if (!TDirection.IsBoundary(this.deblockingState, HevcPlane.Y, lumaX, lumaY))
{
continue;
}
int rasterAddress = ((lumaY / codingTreeBlockSize) * tileLayout.Width) + (lumaX / codingTreeBlockSize);
HevcLoopFilterRegion region = this.sampleAdaptiveOffsetState.GetLoopFilterRegion(rasterAddress, HevcPlane.Y);
if (region.DeblockingFilterDisabled
|| !this.IsDeblockingCtbBoundaryAvailable<TDirection>(
rasterAddress,
HevcPlane.Y,
lumaX,
lumaY,
codingTreeBlockSize,
in tileLayout))
{
continue;
}
int pX = lumaX - (TDirection.IsVertical ? 1 : 0);
int pY = lumaY - (TDirection.IsVertical ? 0 : 1);
int qX = lumaX;
int qY = lumaY;
int quantizationParameterP = codingTreeState.GetQuantizationParameter(pX, pY);
int quantizationParameterQ = codingTreeState.GetQuantizationParameter(qX, qY);
int averageQuantizationParameter = (quantizationParameterP + quantizationParameterQ + 1) >> 1;
// Chroma deblocking uses only the picture-level component offset. Slice offsets and the RExt
// coding-unit adjustment affect inverse quantization, but H.265 excludes both from tc derivation.
int componentOffset = plane == HevcPlane.Cb
? this.pictureParameterSet.ChromaCbQuantizationParameterOffset
: this.pictureParameterSet.ChromaCrQuantizationParameterOffset;
int chromaQuantizationParameter = HevcQuantizationParameters.GetChromaQuantizationParameter(
averageQuantizationParameter,
componentOffset,
0,
this.sequenceParameterSet.ChromaFormat);
int tcIndex = Math.Clamp(chromaQuantizationParameter + 2 + (region.DeblockingFilterTcOffsetDiv2 << 1), 0, 53);
int tc = DeblockingTcTable[tcIndex] * bitDepthScale;
bool partPNoFilter = this.IsDeblockingSuppressed(codingTreeState, pX, pY);
bool partQNoFilter = this.IsDeblockingSuppressed(codingTreeState, qX, qY);
TDirection.FilterChroma(this.Picture, plane, x, y, tc, partPNoFilter, partQNoFilter, bitDepth, segmentLength);
}
}
}
/// <summary>
/// Gets whether an edge crossing a coding-tree-block boundary is permitted by slice and tile rules.
/// </summary>
/// <typeparam name="TDirection">The vertical or horizontal boundary operator.</typeparam>
/// <param name="rasterAddress">The Q-side coding-tree-block raster address.</param>
/// <param name="plane">The primary coding plane.</param>
/// <param name="x">The edge luma X coordinate.</param>
/// <param name="y">The edge luma Y coordinate.</param>
/// <param name="codingTreeBlockSize">The coding-tree-block side in luma samples.</param>
/// <param name="tileLayout">The picture tile mapping.</param>
/// <returns><see langword="true"/> for an internal or permitted external boundary; otherwise, <see langword="false"/>.</returns>
private bool IsDeblockingCtbBoundaryAvailable<TDirection>(
int rasterAddress,
HevcPlane plane,
int x,
int y,
int codingTreeBlockSize,
in HevcTileLayout tileLayout)
where TDirection : struct, IDeblockingDirection
{
int acrossCoordinate = TDirection.IsVertical ? x : y;
if (acrossCoordinate % codingTreeBlockSize != 0)
{
return true;
}
HevcLoopFilterBoundaryAvailability availability = this.sampleAdaptiveOffsetState.GetLoopFilterBoundaryAvailability(
rasterAddress,
plane,
tileLayout.Width,
tileLayout.Height,
this.pictureParameterSet.LoopFilterAcrossTilesEnabled);
return TDirection.IsVertical ? availability.Left : availability.Above;
}
/// <summary>
/// Gets whether PCM or transform-bypass syntax preserves one side of a filtered boundary.
/// </summary>
/// <param name="state">The coding-tree state for the selected primary plane.</param>
/// <param name="x">The luma sample X coordinate.</param>
/// <param name="y">The luma sample Y coordinate.</param>
/// <returns><see langword="true"/> when the reconstructed side must not be modified; otherwise, <see langword="false"/>.</returns>
private bool IsDeblockingSuppressed(HevcCodingTreeState state, int x, int y)
=> (this.sequenceParameterSet.PcmLoopFilterDisabled && state.IsPcm(x, y))
|| (this.pictureParameterSet.TransquantizationBypassEnabled && state.IsTransquantBypass(x, y));
/// <summary>
/// Selects vertical boundary lookup and filtering.
/// </summary>
private readonly struct VerticalDeblockingDirection : IDeblockingDirection
{
/// <inheritdoc/>
public static bool IsVertical => true;
/// <inheritdoc/>
public static bool IsBoundary(HevcDeblockingState state, HevcPlane plane, int x, int y)
=> state.IsVerticalBoundary(plane, x, y);
/// <inheritdoc/>
public static void FilterLuma(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int beta,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth)
=> HevcDeblockingFilter.FilterVerticalLuma(picture, plane, x, y, beta, tc, partPNoFilter, partQNoFilter, bitDepth);
/// <inheritdoc/>
public static void FilterChroma(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth,
int count)
=> HevcDeblockingFilter.FilterVerticalChroma(picture, plane, x, y, tc, partPNoFilter, partQNoFilter, bitDepth, count);
}
/// <summary>
/// Selects horizontal boundary lookup and filtering.
/// </summary>
private readonly struct HorizontalDeblockingDirection : IDeblockingDirection
{
/// <inheritdoc/>
public static bool IsVertical => false;
/// <inheritdoc/>
public static bool IsBoundary(HevcDeblockingState state, HevcPlane plane, int x, int y)
=> state.IsHorizontalBoundary(plane, x, y);
/// <inheritdoc/>
public static void FilterLuma(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int beta,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth)
=> HevcDeblockingFilter.FilterHorizontalLuma(picture, plane, x, y, beta, tc, partPNoFilter, partQNoFilter, bitDepth);
/// <inheritdoc/>
public static void FilterChroma(
HevcPictureBuffer picture,
HevcPlane plane,
int x,
int y,
int tc,
bool partPNoFilter,
bool partQNoFilter,
int bitDepth,
int count)
=> HevcDeblockingFilter.FilterHorizontalChroma(picture, plane, x, y, tc, partPNoFilter, partQNoFilter, bitDepth, count);
}
}

232
src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Prediction.cs

@ -1,232 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Implements intra prediction, reconstructed-plane writes, and PCM sample reconstruction.
/// </content>
internal sealed partial class HevcPictureDecoder
{
/// <summary>
/// Reconstructs one packed intra-prediction block in caller-owned scratch.
/// </summary>
/// <param name="plane">The reconstructed component plane.</param>
/// <param name="x">The prediction-block left coordinate in component samples.</param>
/// <param name="y">The prediction-block top coordinate in component samples.</param>
/// <param name="log2Size">The base-two logarithm of the square prediction-block side.</param>
/// <param name="regionId">The current independent-slice and tile prediction region.</param>
/// <param name="colorPlaneIndex">The selected separate-color plane, or zero for combined coding.</param>
/// <param name="transquantBypass">Whether the governing coding unit bypasses inverse quantization and transform.</param>
/// <returns>The packed predicted samples.</returns>
private Span<ushort> PredictComponentBlock(HevcPlane plane, int x, int y, int log2Size, int regionId, int colorPlaneIndex, bool transquantBypass)
{
int size = 1 << log2Size;
int sampleCount = size * size;
int referenceLength = (size * 2) + 1;
Span<ushort> scratch = this.predictionScratch.Memory.Span;
Span<ushort> prediction = scratch[..sampleCount];
Span<ushort> top = scratch.Slice(MaximumTransformSampleCount, MaximumReferenceLength);
Span<ushort> left = scratch.Slice(MaximumTransformSampleCount + MaximumReferenceLength, MaximumReferenceLength);
Span<ushort> filteredTop = scratch.Slice(MaximumTransformSampleCount + (MaximumReferenceLength * 2), MaximumReferenceLength);
Span<ushort> filteredLeft = scratch.Slice(MaximumTransformSampleCount + (MaximumReferenceLength * 3), MaximumReferenceLength);
int referenceScratchOffset = MaximumTransformSampleCount + (MaximumReferenceLength * 4);
int unitWidth = this.reconstructionState.GetUnitWidth(plane);
int unitHeight = this.reconstructionState.GetUnitHeight(plane);
int referenceScratchLength = HevcIntraPredictor.GetReferenceScratchLength(log2Size, unitWidth);
Span<ushort> referenceScratch = scratch.Slice(referenceScratchOffset, referenceScratchLength);
Span<ushort> operationScratch = scratch[(referenceScratchOffset + referenceScratchLength)..];
Span<bool> availability = this.availabilityScratch.Memory.Span;
int availabilityCount = this.reconstructionState.BuildReferenceAvailability(
plane,
x,
y,
log2Size,
regionId,
availability);
HevcIntraPredictor.PrepareReferenceSamples(
this.Picture,
plane,
x,
y,
log2Size,
unitWidth,
unitHeight,
availability[..availabilityCount],
top,
left,
referenceScratch);
int lumaX = x << this.Picture.GetSubsamplingX(plane);
int lumaY = y << this.Picture.GetSubsamplingY(plane);
bool useLumaSyntax = plane == HevcPlane.Y || this.sequenceParameterSet.SeparateColorPlaneFlag;
int mode = useLumaSyntax
? this.intraPredictionStates[colorPlaneIndex].GetLumaMode(lumaX, lumaY)
: this.intraPredictionStates[colorPlaneIndex].GetEffectiveChromaMode(lumaX, lumaY);
if (!useLumaSyntax && this.sequenceParameterSet.ChromaFormat == 2)
{
mode = HevcIntraPredictionMode.RemapChroma422(mode);
}
// H.265 8.4.4.2.3 and 8.4.4.2.6 restrict prediction-edge filtering to luma blocks no larger than 16 samples.
// Implicit RDPCM bypasses that filtering for the lossless horizontal and vertical prediction modes.
bool filterPredictionEdges = useLumaSyntax
&& size <= 16
&& !(transquantBypass
&& this.sequenceParameterSet.ImplicitResidualDpcmEnabled
&& (mode == HevcIntraPredictionMode.Horizontal || mode == HevcIntraPredictionMode.Vertical));
bool filterReferences = HevcIntraPredictor.ShouldFilterReferenceSamples(
useLumaSyntax ? HevcPlane.Y : plane,
mode,
log2Size,
this.sequenceParameterSet.ChromaFormat,
this.sequenceParameterSet.IntraSmoothingDisabled);
ReadOnlySpan<ushort> selectedTop = top[..referenceLength];
ReadOnlySpan<ushort> selectedLeft = left[..referenceLength];
if (filterReferences)
{
// Normal three-tap smoothing extends to combined 4:4:4 chroma, but strong bilinear smoothing is a luma
// operation. Separate color planes use luma syntax and therefore retain the luma behavior.
bool useStrongSmoothing = useLumaSyntax && this.sequenceParameterSet.StrongIntraSmoothingEnabled;
HevcIntraPredictor.FilterReferenceSamples(
selectedTop,
selectedLeft,
filteredTop,
filteredLeft,
log2Size,
this.Picture.GetBitDepth(plane),
useStrongSmoothing);
selectedTop = filteredTop[..referenceLength];
selectedLeft = filteredLeft[..referenceLength];
}
HevcIntraPredictor.Predict(
selectedTop,
selectedLeft,
prediction,
size,
log2Size,
mode,
this.Picture.GetBitDepth(plane),
filterPredictionEdges,
operationScratch);
return prediction;
}
/// <summary>
/// Copies one packed reconstructed block into the allocator-owned picture plane.
/// </summary>
/// <param name="source">The packed reconstructed samples.</param>
/// <param name="plane">The destination component plane.</param>
/// <param name="x">The destination left coordinate.</param>
/// <param name="y">The destination top coordinate.</param>
/// <param name="size">The square block side.</param>
private void CopyPredictionToPicture(ReadOnlySpan<ushort> source, HevcPlane plane, int x, int y, int size)
{
for (int row = 0; row < size; row++)
{
source.Slice(row * size, size).CopyTo(this.Picture.GetRowSpan(plane, y + row)[x..]);
}
}
/// <summary>
/// Reads and writes every raw sample in one PCM coding unit before arithmetic decoding restarts.
/// </summary>
/// <param name="reader">The suspended entropy-substream reader.</param>
/// <param name="x">The coding-unit left luma coordinate.</param>
/// <param name="y">The coding-unit top luma coordinate.</param>
/// <param name="log2Size">The base-two logarithm of the coding-unit side.</param>
/// <param name="regionId">The current independent-slice and tile prediction region.</param>
/// <param name="colorPlaneIndex">The selected separate-color plane, or zero for combined coding.</param>
private void DecodePcmCodingUnit(
ref HevcCabacSyntaxReader reader,
int x,
int y,
int log2Size,
int regionId,
int colorPlaneIndex)
{
int size = 1 << log2Size;
if (this.sequenceParameterSet.SeparateColorPlaneFlag)
{
HevcPlane plane = (HevcPlane)colorPlaneIndex;
this.DecodePcmPlane(ref reader, plane, x, y, size, size, this.sequenceParameterSet.PcmBitDepthLuma, regionId);
return;
}
this.DecodePcmPlane(ref reader, HevcPlane.Y, x, y, size, size, this.sequenceParameterSet.PcmBitDepthLuma, regionId);
if (this.sequenceParameterSet.ChromaFormat == 0)
{
return;
}
int subsamplingX = this.Picture.GetSubsamplingX(HevcPlane.Cb);
int subsamplingY = this.Picture.GetSubsamplingY(HevcPlane.Cb);
int chromaWidth = size >> subsamplingX;
int chromaHeight = size >> subsamplingY;
int chromaX = x >> subsamplingX;
int chromaY = y >> subsamplingY;
this.DecodePcmPlane(
ref reader,
HevcPlane.Cb,
chromaX,
chromaY,
chromaWidth,
chromaHeight,
this.sequenceParameterSet.PcmBitDepthChroma,
regionId);
this.DecodePcmPlane(
ref reader,
HevcPlane.Cr,
chromaX,
chromaY,
chromaWidth,
chromaHeight,
this.sequenceParameterSet.PcmBitDepthChroma,
regionId);
}
/// <summary>
/// Reads one rectangular PCM component plane directly into the reconstructed picture.
/// </summary>
/// <param name="reader">The suspended entropy-substream reader.</param>
/// <param name="plane">The destination component plane.</param>
/// <param name="x">The destination left coordinate.</param>
/// <param name="y">The destination top coordinate.</param>
/// <param name="width">The component rectangle width.</param>
/// <param name="height">The component rectangle height.</param>
/// <param name="bitDepth">The PCM sample precision.</param>
/// <param name="regionId">The current independent-slice and tile prediction region.</param>
private void DecodePcmPlane(
ref HevcCabacSyntaxReader reader,
HevcPlane plane,
int x,
int y,
int width,
int height,
int bitDepth,
int regionId)
{
// PCM samples can use fewer bits than the reconstructed component. H.265 places those bits at the
// most-significant end of the component range, so the raw code value must be restored before filtering.
int bitDepthShift = this.Picture.GetBitDepth(plane) - bitDepth;
for (int row = 0; row < height; row++)
{
Span<ushort> destination = this.Picture.GetRowSpan(plane, y + row).Slice(x, width);
for (int column = 0; column < width; column++)
{
destination[column] = (ushort)(reader.ReadPcmSample(bitDepth) << bitDepthShift);
}
}
this.reconstructionState.MarkReconstructed(plane, x, y, width, height, regionId);
}
}

250
src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.SampleAdaptiveOffset.cs

@ -1,250 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Implements sample-adaptive-offset syntax decoding and merge resolution.
/// </content>
internal sealed partial class HevcPictureDecoder
{
/// <summary>
/// Applies the resolved sample-adaptive offsets to every component after deblocking has completed.
/// </summary>
/// <param name="source">The immutable deblocked picture used to classify every sample.</param>
/// <param name="tileLayout">The picture tile mapping used to derive coding-tree-block boundaries.</param>
private void ApplySampleAdaptiveOffset(HevcPictureBuffer source, in HevcTileLayout tileLayout)
{
int codingTreeBlockSize = 1 << this.sequenceParameterSet.CodingTreeBlockLog2;
int planeCount = this.sequenceParameterSet.ChromaFormat == 0 ? 1 : 3;
for (int planeIndex = 0; planeIndex < planeCount; planeIndex++)
{
HevcPlane plane = (HevcPlane)planeIndex;
HevcPlane regionPlane = this.sequenceParameterSet.SeparateColorPlaneFlag ? plane : HevcPlane.Y;
int subsamplingX = this.Picture.GetSubsamplingX(plane);
int subsamplingY = this.Picture.GetSubsamplingY(plane);
int blockWidth = codingTreeBlockSize >> subsamplingX;
int blockHeight = codingTreeBlockSize >> subsamplingY;
int planeWidth = this.Picture.GetWidth(plane);
int planeHeight = this.Picture.GetHeight(plane);
int offsetScaleLog2 = plane == HevcPlane.Y
? this.pictureParameterSet.SampleAdaptiveOffsetScaleLumaLog2
: this.pictureParameterSet.SampleAdaptiveOffsetScaleChromaLog2;
for (int codingTreeBlockY = 0; codingTreeBlockY < tileLayout.Height; codingTreeBlockY++)
{
for (int codingTreeBlockX = 0; codingTreeBlockX < tileLayout.Width; codingTreeBlockX++)
{
int rasterAddress = (codingTreeBlockY * tileLayout.Width) + codingTreeBlockX;
HevcSampleAdaptiveOffsetParameters parameters = this.sampleAdaptiveOffsetState.Get(rasterAddress, plane);
if (parameters.Type == HevcSampleAdaptiveOffsetType.Off)
{
continue;
}
HevcLoopFilterBoundaryAvailability availability = this.sampleAdaptiveOffsetState.GetLoopFilterBoundaryAvailability(
rasterAddress,
regionPlane,
tileLayout.Width,
tileLayout.Height,
this.pictureParameterSet.LoopFilterAcrossTilesEnabled);
int x = codingTreeBlockX * blockWidth;
int y = codingTreeBlockY * blockHeight;
int width = Math.Min(blockWidth, planeWidth - x);
int height = Math.Min(blockHeight, planeHeight - y);
// Every classification reads the immutable post-deblocking picture. Later CTBs can therefore never
// observe offsets already written by an earlier CTB, including across permitted slice and tile boundaries.
HevcSampleAdaptiveOffsetFilter.ApplyBlock(
source,
this.Picture,
plane,
x,
y,
width,
height,
in parameters,
offsetScaleLog2,
availability.Left,
availability.Right,
availability.Above,
availability.Below,
availability.AboveLeft,
availability.AboveRight,
availability.BelowLeft,
availability.BelowRight);
}
}
}
}
/// <summary>
/// Decodes and resolves the sample-adaptive-offset parameters for one coding-tree block.
/// </summary>
/// <param name="reader">The active entropy-substream reader.</param>
/// <param name="independentSlice">The independent slice governing component enable flags.</param>
/// <param name="rasterAddress">The coding-tree block's raster-scan address.</param>
/// <param name="codingTreeBlockX">The horizontal coding-tree-block coordinate.</param>
/// <param name="codingTreeBlockY">The vertical coding-tree-block coordinate.</param>
/// <param name="regionId">The current independent-slice and tile prediction region.</param>
private void DecodeSampleAdaptiveOffset(
ref HevcCabacSyntaxReader reader,
HevcSliceSegmentHeader independentSlice,
int rasterAddress,
int codingTreeBlockX,
int codingTreeBlockY,
int regionId)
{
HevcPlane regionPlane = this.sequenceParameterSet.SeparateColorPlaneFlag ? (HevcPlane)independentSlice.ColorPlaneId : HevcPlane.Y;
bool lumaEnabled = independentSlice.SampleAdaptiveOffsetLumaEnabled == true;
bool chromaEnabled = independentSlice.SampleAdaptiveOffsetChromaEnabled == true;
if (!lumaEnabled && !chromaEnabled)
{
this.sampleAdaptiveOffsetState.SetRegion(rasterAddress, regionPlane, regionId);
return;
}
int codingTreeBlockWidth = HevcParameterSetSyntax.GetCodingTreeBlockCount(
this.sequenceParameterSet.Width,
this.sequenceParameterSet.CodingTreeBlockLog2);
int leftAddress = rasterAddress - 1;
bool leftAvailable = codingTreeBlockX > 0 && this.sampleAdaptiveOffsetState.IsInRegion(leftAddress, regionPlane, regionId);
bool mergeLeft = leftAvailable && reader.ReadSampleAdaptiveOffsetMerge();
int aboveAddress = rasterAddress - codingTreeBlockWidth;
bool aboveAvailable = codingTreeBlockY > 0 && this.sampleAdaptiveOffsetState.IsInRegion(aboveAddress, regionPlane, regionId);
bool mergeAbove = !mergeLeft && aboveAvailable && reader.ReadSampleAdaptiveOffsetMerge();
if (mergeLeft || mergeAbove)
{
int sourceAddress = mergeLeft ? leftAddress : aboveAddress;
this.CopySampleAdaptiveOffsetParameters(sourceAddress, rasterAddress, lumaEnabled, chromaEnabled, independentSlice.ColorPlaneId);
this.sampleAdaptiveOffsetState.SetRegion(rasterAddress, regionPlane, regionId);
return;
}
if (this.sequenceParameterSet.SeparateColorPlaneFlag)
{
HevcPlane plane = (HevcPlane)independentSlice.ColorPlaneId;
this.sampleAdaptiveOffsetState.Set(rasterAddress, plane, ReadSampleAdaptiveOffsetParameters(ref reader, this.Picture.GetBitDepth(plane), -1));
}
else
{
if (lumaEnabled)
{
this.sampleAdaptiveOffsetState.Set(
rasterAddress,
HevcPlane.Y,
ReadSampleAdaptiveOffsetParameters(ref reader, this.sequenceParameterSet.BitDepthLuma, -1));
}
if (chromaEnabled)
{
HevcSampleAdaptiveOffsetParameters chromaBlue = ReadSampleAdaptiveOffsetParameters(
ref reader,
this.sequenceParameterSet.BitDepthChroma,
-1);
this.sampleAdaptiveOffsetState.Set(rasterAddress, HevcPlane.Cb, chromaBlue);
this.sampleAdaptiveOffsetState.Set(
rasterAddress,
HevcPlane.Cr,
ReadSampleAdaptiveOffsetParameters(ref reader, this.sequenceParameterSet.BitDepthChroma, (int)chromaBlue.Type));
}
}
this.sampleAdaptiveOffsetState.SetRegion(rasterAddress, regionPlane, regionId);
}
/// <summary>
/// Copies resolved merge-source parameters for the components enabled by the current slice.
/// </summary>
/// <param name="sourceAddress">The merge-source coding-tree-block address.</param>
/// <param name="destinationAddress">The current coding-tree-block address.</param>
/// <param name="lumaEnabled">Whether the current slice enables luma sample-adaptive offset.</param>
/// <param name="chromaEnabled">Whether the current slice enables chroma sample-adaptive offset.</param>
/// <param name="colorPlaneId">The selected separate-color-plane identifier.</param>
private void CopySampleAdaptiveOffsetParameters(
int sourceAddress,
int destinationAddress,
bool lumaEnabled,
bool chromaEnabled,
byte colorPlaneId)
{
if (this.sequenceParameterSet.SeparateColorPlaneFlag)
{
HevcPlane plane = (HevcPlane)colorPlaneId;
this.sampleAdaptiveOffsetState.Set(destinationAddress, plane, this.sampleAdaptiveOffsetState.Get(sourceAddress, plane));
return;
}
if (lumaEnabled)
{
this.sampleAdaptiveOffsetState.Set(destinationAddress, HevcPlane.Y, this.sampleAdaptiveOffsetState.Get(sourceAddress, HevcPlane.Y));
}
if (chromaEnabled)
{
this.sampleAdaptiveOffsetState.Set(destinationAddress, HevcPlane.Cb, this.sampleAdaptiveOffsetState.Get(sourceAddress, HevcPlane.Cb));
this.sampleAdaptiveOffsetState.Set(destinationAddress, HevcPlane.Cr, this.sampleAdaptiveOffsetState.Get(sourceAddress, HevcPlane.Cr));
}
}
/// <summary>
/// Decodes one component's new or disabled sample-adaptive-offset mode.
/// </summary>
/// <param name="reader">The active entropy-substream reader.</param>
/// <param name="bitDepth">The component sample precision.</param>
/// <param name="inheritedType">The Cb type inherited by Cr, or negative one when the type is signaled.</param>
/// <returns>The resolved component parameters.</returns>
private static HevcSampleAdaptiveOffsetParameters ReadSampleAdaptiveOffsetParameters(
ref HevcCabacSyntaxReader reader,
int bitDepth,
int inheritedType)
{
int type = inheritedType >= 0
? inheritedType == (int)HevcSampleAdaptiveOffsetType.Off ? 0 : inheritedType == (int)HevcSampleAdaptiveOffsetType.Band ? 1 : 2
: reader.ReadSampleAdaptiveOffsetType();
if (type == 0)
{
return default;
}
int maximumOffset = (1 << (Math.Min(bitDepth, 10) - 5)) - 1;
int offset0 = reader.ReadSampleAdaptiveOffsetAbsolute(maximumOffset);
int offset1 = reader.ReadSampleAdaptiveOffsetAbsolute(maximumOffset);
int offset2 = reader.ReadSampleAdaptiveOffsetAbsolute(maximumOffset);
int offset3 = reader.ReadSampleAdaptiveOffsetAbsolute(maximumOffset);
if (type == 1)
{
offset0 = ApplySampleAdaptiveOffsetSign(ref reader, offset0);
offset1 = ApplySampleAdaptiveOffsetSign(ref reader, offset1);
offset2 = ApplySampleAdaptiveOffsetSign(ref reader, offset2);
offset3 = ApplySampleAdaptiveOffsetSign(ref reader, offset3);
return new HevcSampleAdaptiveOffsetParameters(
HevcSampleAdaptiveOffsetType.Band,
reader.ReadSampleAdaptiveOffsetBandPosition(),
offset0,
offset1,
offset2,
offset3,
0);
}
HevcSampleAdaptiveOffsetType edgeType = inheritedType >= 0
? (HevcSampleAdaptiveOffsetType)inheritedType
: (HevcSampleAdaptiveOffsetType)((int)HevcSampleAdaptiveOffsetType.EdgeHorizontal + reader.ReadSampleAdaptiveOffsetEdgeClass());
return new HevcSampleAdaptiveOffsetParameters(edgeType, 0, offset0, offset1, 0, -offset2, -offset3);
}
/// <summary>
/// Applies an explicitly coded sign to a nonzero band-offset magnitude.
/// </summary>
/// <param name="reader">The active entropy-substream reader.</param>
/// <param name="magnitude">The decoded unsigned magnitude.</param>
/// <returns>The signed magnitude.</returns>
private static int ApplySampleAdaptiveOffsetSign(ref HevcCabacSyntaxReader reader, int magnitude)
=> magnitude != 0 && reader.ReadSampleAdaptiveOffsetSign() ? -magnitude : magnitude;
}

564
src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.TransformTree.cs

@ -1,564 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Implements transform-tree syntax, coefficient reconstruction, and intra sample reconstruction.
/// </content>
internal sealed partial class HevcPictureDecoder
{
/// <summary>
/// Decodes and reconstructs one transform-tree node.
/// </summary>
/// <param name="reader">The active entropy-substream reader.</param>
/// <param name="geometry">The luma and component rectangles at this transform depth.</param>
/// <param name="transformDepth">The transform depth relative to the coding-unit root.</param>
/// <param name="minimumTransformLog2">The smallest luma transform permitted in the coding unit.</param>
/// <param name="usesNxNPartitions">Whether the coding unit has four luma prediction partitions.</param>
/// <param name="transquantBypass">Whether the coding unit bypasses inverse quantization and transform.</param>
/// <param name="regionId">The current independent-slice and tile prediction region.</param>
/// <param name="colorPlaneIndex">The selected separate-color plane, or zero for combined coding.</param>
/// <param name="parentChromaBlueFlags">The blue-difference coded-block flags inherited from the parent.</param>
/// <param name="parentChromaRedFlags">The red-difference coded-block flags inherited from the parent.</param>
private void DecodeTransformTree(
ref HevcCabacSyntaxReader reader,
in HevcTransformUnitGeometry geometry,
int transformDepth,
int minimumTransformLog2,
bool usesNxNPartitions,
bool transquantBypass,
int regionId,
int colorPlaneIndex,
HevcCodedBlockFlags parentChromaBlueFlags,
HevcCodedBlockFlags parentChromaRedFlags)
{
int log2Size = geometry.Log2LumaSize;
HevcTransformComponentGeometry primaryGeometry = geometry.Primary;
HevcTransformComponentGeometry chromaBlueGeometry = geometry.ChromaBlue;
HevcTransformComponentGeometry chromaRedGeometry = geometry.ChromaRed;
bool split;
if (usesNxNPartitions && transformDepth == 0)
{
split = true;
}
else if (log2Size > this.sequenceParameterSet.MaxTransformBlockLog2)
{
split = true;
}
else if (log2Size == this.sequenceParameterSet.MinTransformBlockLog2 || log2Size == minimumTransformLog2)
{
split = false;
}
else
{
split = reader.ReadTransformSubdivision(log2Size);
}
HevcCodedBlockFlags chromaBlueFlags = parentChromaBlueFlags;
HevcCodedBlockFlags chromaRedFlags = parentChromaRedFlags;
if (geometry.HasCombinedChroma)
{
chromaBlueFlags = DecodeChromaCodedBlockFlags(
ref reader,
in chromaBlueGeometry,
transformDepth,
split,
parentChromaBlueFlags);
chromaRedFlags = DecodeChromaCodedBlockFlags(
ref reader,
in chromaRedGeometry,
transformDepth,
split,
parentChromaRedFlags);
}
if (split)
{
for (int child = 0; child < 4; child++)
{
HevcTransformUnitGeometry childGeometry = geometry.CreateChild(child);
this.DecodeTransformTree(
ref reader,
in childGeometry,
transformDepth + 1,
minimumTransformLog2,
usesNxNPartitions,
transquantBypass,
regionId,
colorPlaneIndex,
chromaBlueFlags,
chromaRedFlags);
}
return;
}
this.deblockingState.MarkBlock(
geometry.PrimaryPlane,
primaryGeometry.X,
primaryGeometry.Y,
primaryGeometry.Width,
primaryGeometry.Height);
HevcCodedBlockFlags primaryFlags = new(reader.ReadTransformCodedBlockFlag(false, transformDepth == 0 ? 1 : 0));
bool hasCodedResidual = primaryFlags.Any || chromaBlueFlags.Any || chromaRedFlags.Any;
if (hasCodedResidual && this.quantizationParameterDeltaPending)
{
this.ApplyQuantizationParameterDelta(reader.ReadDeltaQuantizationParameter());
this.quantizationParameterDeltaPending = false;
}
if ((chromaBlueFlags.Any || chromaRedFlags.Any)
&& this.chromaQuantizationAdjustmentPending
&& !transquantBypass)
{
this.currentChromaQuantizationAdjustment = reader.ReadChromaQuantizationAdjustment(
this.pictureParameterSet.ChromaQuantizationParameterOffsetsCb.Count);
this.chromaQuantizationAdjustmentPending = false;
}
HevcQuantizationParameters quantizationParameters = this.CreateQuantizationParameters();
Span<int> lumaResidual = this.integerScratch.Memory.Span.Slice(MaximumTransformSampleCount * 3, MaximumTransformSampleCount);
lumaResidual.Clear();
this.DecodeComponentSections(
ref reader,
geometry.PrimaryPlane,
in primaryGeometry,
primaryFlags,
transquantBypass,
regionId,
colorPlaneIndex,
in quantizationParameters,
lumaResidual,
true,
0,
in primaryGeometry);
if (!geometry.HasCombinedChroma)
{
return;
}
int chromaMode = this.intraPredictionStates[colorPlaneIndex].GetChromaMode(geometry.Primary.X, geometry.Primary.Y);
int chromaBlueAlpha = 0;
bool canPredictAcrossComponents = this.pictureParameterSet.CrossComponentPredictionEnabled
&& primaryFlags.Any
&& chromaMode == 36
&& chromaBlueGeometry.Process
&& chromaBlueGeometry.Width == chromaBlueGeometry.Height;
if (canPredictAcrossComponents)
{
chromaBlueAlpha = reader.ReadCrossComponentPredictionScale(0);
}
this.DecodeComponentSections(
ref reader,
HevcPlane.Cb,
in chromaBlueGeometry,
chromaBlueFlags,
transquantBypass,
regionId,
colorPlaneIndex,
in quantizationParameters,
lumaResidual,
false,
chromaBlueAlpha,
in primaryGeometry);
int chromaRedAlpha = 0;
if (canPredictAcrossComponents)
{
// The Cr scale follows the complete Cb residual syntax. Reading both scales together changes every
// subsequent CABAC decision whenever Cb carries coefficients.
chromaRedAlpha = reader.ReadCrossComponentPredictionScale(1);
}
this.DecodeComponentSections(
ref reader,
HevcPlane.Cr,
in chromaRedGeometry,
chromaRedFlags,
transquantBypass,
regionId,
colorPlaneIndex,
in quantizationParameters,
lumaResidual,
false,
chromaRedAlpha,
in primaryGeometry);
}
/// <summary>
/// Decodes chroma coded-block flags at the highest transform level that owns the component rectangle.
/// </summary>
/// <param name="reader">The active entropy-substream reader.</param>
/// <param name="geometry">The current chroma component rectangle.</param>
/// <param name="transformDepth">The luma transform depth.</param>
/// <param name="lumaSplit">Whether the current luma transform node subdivides.</param>
/// <param name="parentFlags">The coded-block flags inherited from the parent transform node.</param>
/// <returns>The flags governing the current component rectangle.</returns>
private static HevcCodedBlockFlags DecodeChromaCodedBlockFlags(
ref HevcCabacSyntaxReader reader,
in HevcTransformComponentGeometry geometry,
int transformDepth,
bool lumaSplit,
HevcCodedBlockFlags parentFlags)
{
if (!geometry.Process)
{
return parentFlags;
}
bool shouldDecode = transformDepth == 0 || (geometry.ProcessesAllQuadrants && parentFlags.Any);
if (!shouldDecode)
{
return parentFlags;
}
int context = transformDepth;
bool canQuadSplit = geometry.Width >= 8 && geometry.Height >= 8;
if (geometry.Width != geometry.Height && (!lumaSplit || !canQuadSplit))
{
bool first = reader.ReadTransformCodedBlockFlag(true, context);
bool second = reader.ReadTransformCodedBlockFlag(true, context);
return new HevcCodedBlockFlags(first, second);
}
return new HevcCodedBlockFlags(reader.ReadTransformCodedBlockFlag(true, context));
}
/// <summary>
/// Decodes one square component block or the two square sub-blocks of a rectangular 4:2:2 transform section.
/// </summary>
/// <param name="reader">The active entropy-substream reader.</param>
/// <param name="plane">The reconstructed component plane.</param>
/// <param name="geometry">The component rectangle.</param>
/// <param name="codedBlockFlags">The component coded-block flags.</param>
/// <param name="transquantBypass">Whether the coding unit bypasses inverse quantization and transform.</param>
/// <param name="regionId">The current independent-slice and tile prediction region.</param>
/// <param name="colorPlaneIndex">The selected separate-color plane, or zero for combined coding.</param>
/// <param name="quantizationParameters">The effective component quantization parameters.</param>
/// <param name="lumaResidual">The current luma residual retained for cross-component prediction.</param>
/// <param name="retainResidual">Whether reconstructed residuals are copied to <paramref name="lumaResidual"/>.</param>
/// <param name="crossComponentAlpha">The signed inverse cross-component prediction scale.</param>
/// <param name="lumaGeometry">The luma transform rectangle governing cross-component residual addressing.</param>
private void DecodeComponentSections(
ref HevcCabacSyntaxReader reader,
HevcPlane plane,
in HevcTransformComponentGeometry geometry,
HevcCodedBlockFlags codedBlockFlags,
bool transquantBypass,
int regionId,
int colorPlaneIndex,
in HevcQuantizationParameters quantizationParameters,
Span<int> lumaResidual,
bool retainResidual,
int crossComponentAlpha,
in HevcTransformComponentGeometry lumaGeometry)
{
if (!geometry.Process)
{
return;
}
if (geometry.Width == geometry.Height)
{
this.DecodeComponentBlock(
ref reader,
plane,
geometry.X,
geometry.Y,
geometry.Width,
codedBlockFlags.First,
transquantBypass,
regionId,
colorPlaneIndex,
in quantizationParameters,
lumaResidual,
retainResidual,
crossComponentAlpha,
this.GetLumaResidualOffset(plane, geometry.X, geometry.Y, in lumaGeometry),
lumaGeometry.Width);
return;
}
int size = Math.Min(geometry.Width, geometry.Height);
int secondX = geometry.Width > geometry.Height ? geometry.X + size : geometry.X;
int secondY = geometry.Height > geometry.Width ? geometry.Y + size : geometry.Y;
this.DecodeComponentBlock(
ref reader,
plane,
geometry.X,
geometry.Y,
size,
codedBlockFlags.First,
transquantBypass,
regionId,
colorPlaneIndex,
in quantizationParameters,
lumaResidual,
retainResidual,
crossComponentAlpha,
this.GetLumaResidualOffset(plane, geometry.X, geometry.Y, in lumaGeometry),
lumaGeometry.Width);
this.DecodeComponentBlock(
ref reader,
plane,
secondX,
secondY,
size,
codedBlockFlags.Second,
transquantBypass,
regionId,
colorPlaneIndex,
in quantizationParameters,
lumaResidual,
retainResidual,
crossComponentAlpha,
this.GetLumaResidualOffset(plane, secondX, secondY, in lumaGeometry),
lumaGeometry.Width);
}
/// <summary>
/// Decodes, predicts, and reconstructs one square transform block.
/// </summary>
/// <param name="reader">The active entropy-substream reader.</param>
/// <param name="plane">The reconstructed component plane.</param>
/// <param name="x">The block left coordinate in component samples.</param>
/// <param name="y">The block top coordinate in component samples.</param>
/// <param name="size">The square transform-block side.</param>
/// <param name="codedBlockFlag">Whether coefficient syntax is present.</param>
/// <param name="transquantBypass">Whether the coding unit bypasses inverse quantization and transform.</param>
/// <param name="regionId">The current independent-slice and tile prediction region.</param>
/// <param name="colorPlaneIndex">The selected separate-color plane, or zero for combined coding.</param>
/// <param name="quantizationParameters">The effective component quantization parameters.</param>
/// <param name="lumaResidual">The current luma residual retained for cross-component prediction.</param>
/// <param name="retainResidual">Whether reconstructed residuals are copied to <paramref name="lumaResidual"/>.</param>
/// <param name="crossComponentAlpha">The signed inverse cross-component prediction scale.</param>
/// <param name="lumaResidualOffset">The first colocated sample in the retained luma residual.</param>
/// <param name="lumaResidualStride">The retained luma residual row stride.</param>
private void DecodeComponentBlock(
ref HevcCabacSyntaxReader reader,
HevcPlane plane,
int x,
int y,
int size,
bool codedBlockFlag,
bool transquantBypass,
int regionId,
int colorPlaneIndex,
in HevcQuantizationParameters quantizationParameters,
Span<int> lumaResidual,
bool retainResidual,
int crossComponentAlpha,
int lumaResidualOffset,
int lumaResidualStride)
{
int log2Size = BitOperations.Log2((uint)size);
int sampleCount = size * size;
Span<int> integerScratch = this.integerScratch.Memory.Span;
Span<int> quantized = integerScratch[..MaximumTransformSampleCount];
Span<int> dequantized = integerScratch.Slice(MaximumTransformSampleCount, MaximumTransformSampleCount);
Span<int> residual = integerScratch.Slice(MaximumTransformSampleCount * 2, MaximumTransformSampleCount);
Span<int> transformScratch = integerScratch.Slice(MaximumTransformSampleCount * 4, MaximumTransformSampleCount * 2);
Span<ushort> prediction = this.PredictComponentBlock(plane, x, y, log2Size, regionId, colorPlaneIndex, transquantBypass);
residual[..sampleCount].Clear();
bool useLumaSyntax = this.sequenceParameterSet.SeparateColorPlaneFlag;
HevcPlane codingPlane = useLumaSyntax ? HevcPlane.Y : plane;
int lumaX = x << this.Picture.GetSubsamplingX(plane);
int lumaY = y << this.Picture.GetSubsamplingY(plane);
int codingPredictionMode = plane == HevcPlane.Y || useLumaSyntax
? this.intraPredictionStates[colorPlaneIndex].GetLumaMode(lumaX, lumaY)
: this.intraPredictionStates[colorPlaneIndex].GetEffectiveChromaMode(lumaX, lumaY);
int predictionMode = codingPredictionMode;
if (plane != HevcPlane.Y && !useLumaSyntax && this.sequenceParameterSet.ChromaFormat == 2)
{
predictionMode = HevcIntraPredictionMode.RemapChroma422(predictionMode);
}
bool transformSkip = codedBlockFlag
&& !transquantBypass
&& this.pictureParameterSet.TransformSkipEnabled
&& log2Size <= this.pictureParameterSet.MaxTransformSkipBlockLog2
&& reader.ReadTransformSkip(codingPlane != HevcPlane.Y);
HevcResidualDpcmMode residualDpcmMode = this.sequenceParameterSet.ImplicitResidualDpcmEnabled && (transformSkip || transquantBypass)
? HevcResidualReconstructor.GetImplicitResidualDpcmMode(predictionMode, false)
: HevcResidualDpcmMode.None;
if (codedBlockFlag)
{
HevcCoefficientCodingParameters codingParameters = HevcCoefficientCodingParameters.Create(
this.pictureParameterSet,
size,
size,
plane,
true,
codingPredictionMode,
transformSkip,
transquantBypass,
residualDpcmMode,
useLumaSyntax);
this.coefficientDecoder.Decode(ref reader, quantized, in codingParameters);
bool rotate = HevcResidualReconstructor.IsNonTransformedResidualRotated(
this.sequenceParameterSet.TransformSkipRotationEnabled,
true,
size);
if (transquantBypass)
{
HevcResidualReconstructor.CopyBypassed(quantized[..sampleCount], residual, rotate);
}
else
{
int bitDepth = this.Picture.GetBitDepth(plane);
int maxTransformDynamicRange = this.sequenceParameterSet.GetMaxTransformDynamicRange(codingPlane);
int quantizationParameter = useLumaSyntax
? quantizationParameters.Luma
: quantizationParameters.Get(plane);
HevcInverseQuantizer.Dequantize(
quantized,
dequantized,
log2Size,
bitDepth,
maxTransformDynamicRange,
quantizationParameter,
this.sequenceParameterSet.ScalingListEnabled,
this.pictureParameterSet.ScalingList,
codingPlane,
true,
transformSkip,
this.sequenceParameterSet.ExtendedPrecisionProcessingEnabled);
if (transformSkip)
{
HevcResidualReconstructor.ApplyTransformSkip(
dequantized,
residual,
size,
size,
bitDepth,
maxTransformDynamicRange,
log2Size,
this.sequenceParameterSet.ExtendedPrecisionProcessingEnabled,
rotate);
}
else
{
HevcInverseTransformer.Transform(
dequantized,
residual,
log2Size,
log2Size,
bitDepth,
maxTransformDynamicRange,
codingPlane == HevcPlane.Y && log2Size == 2,
transformScratch);
}
}
HevcResidualReconstructor.ApplyResidualDpcm(residual, size, size, residualDpcmMode);
}
if (crossComponentAlpha != 0)
{
for (int row = 0; row < size; row++)
{
HevcResidualReconstructor.ApplyCrossComponentPrediction(
lumaResidual.Slice(lumaResidualOffset + (row * lumaResidualStride), size),
residual.Slice(row * size, size),
size,
crossComponentAlpha,
this.sequenceParameterSet.BitDepthLuma - this.sequenceParameterSet.BitDepthChroma);
}
}
if (retainResidual)
{
for (int row = 0; row < size; row++)
{
residual.Slice(row * size, size).CopyTo(lumaResidual.Slice(lumaResidualOffset + (row * lumaResidualStride), size));
}
}
HevcInverseTransformer.AddResidual(
residual,
prediction,
size,
size,
size,
this.Picture.GetBitDepth(plane));
this.CopyPredictionToPicture(prediction, plane, x, y, size);
this.reconstructionState.MarkReconstructed(plane, x, y, size, size, regionId);
}
/// <summary>
/// Gets the packed luma-residual offset colocated with one component block.
/// </summary>
/// <param name="plane">The component plane.</param>
/// <param name="x">The component block left coordinate.</param>
/// <param name="y">The component block top coordinate.</param>
/// <param name="lumaGeometry">The governing luma transform rectangle.</param>
/// <returns>The zero-based packed luma-residual offset.</returns>
private int GetLumaResidualOffset(HevcPlane plane, int x, int y, in HevcTransformComponentGeometry lumaGeometry)
{
int lumaX = x << this.Picture.GetSubsamplingX(plane);
int lumaY = y << this.Picture.GetSubsamplingY(plane);
return ((lumaY - lumaGeometry.Y) * lumaGeometry.Width) + lumaX - lumaGeometry.X;
}
/// <summary>
/// Applies the signed coding-unit luma quantization delta with bit-depth-dependent modular wrapping.
/// </summary>
/// <param name="delta">The decoded signed delta.</param>
private void ApplyQuantizationParameterDelta(int delta)
{
int bitDepthOffset = 6 * (this.sequenceParameterSet.BitDepthLuma - 8);
int modulus = 52 + bitDepthOffset;
int value = this.currentQuantizationParameter + delta + bitDepthOffset;
value %= modulus;
if (value < 0)
{
value += modulus;
}
this.currentQuantizationParameter = value - bitDepthOffset;
}
/// <summary>
/// Creates the component quantization parameters selected by picture, slice, and coding-unit offsets.
/// </summary>
/// <returns>The effective luma, Cb, and Cr quantization parameters.</returns>
private HevcQuantizationParameters CreateQuantizationParameters()
{
int cbOffset = this.pictureParameterSet.ChromaCbQuantizationParameterOffset + this.currentSliceChromaBlueQuantizationOffset;
int crOffset = this.pictureParameterSet.ChromaCrQuantizationParameterOffset + this.currentSliceChromaRedQuantizationOffset;
if (this.currentChromaQuantizationAdjustment > 0)
{
int adjustmentIndex = this.currentChromaQuantizationAdjustment - 1;
cbOffset += this.pictureParameterSet.ChromaQuantizationParameterOffsetsCb[adjustmentIndex];
crOffset += this.pictureParameterSet.ChromaQuantizationParameterOffsetsCr[adjustmentIndex];
}
return new HevcQuantizationParameters(
this.currentQuantizationParameter,
this.sequenceParameterSet.BitDepthLuma,
this.sequenceParameterSet.BitDepthChroma,
this.sequenceParameterSet.ChromaFormat,
cbOffset,
crOffset);
}
}

409
src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.Traversal.cs

@ -1,409 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <content>
/// Implements slice, coding-tree, and coding-unit traversal.
/// </content>
internal sealed partial class HevcPictureDecoder
{
/// <summary>
/// Decodes one ordered slice segment and returns the next tile-scan coding-tree-block address.
/// </summary>
/// <param name="slice">The current independent or dependent slice segment.</param>
/// <param name="independentSlice">The independent header governing inherited slice fields.</param>
/// <param name="independentSliceIndex">The one-based independent-slice index within the selected color plane.</param>
/// <param name="tileLayout">The picture tile mapping.</param>
/// <param name="startAddressInTileScan">The first coding-tree block in tile-scan order.</param>
/// <param name="independentSliceStartAddressInTileScan">The governing independent slice's first coding-tree block in tile-scan order.</param>
/// <returns>The tile-scan address immediately following the decoded segment.</returns>
private int DecodeSliceSegment(
HevcSliceSegmentHeader slice,
HevcSliceSegmentHeader independentSlice,
int independentSliceIndex,
in HevcTileLayout tileLayout,
int startAddressInTileScan,
int independentSliceStartAddressInTileScan)
{
int sliceQuantizationParameter = independentSlice.QuantizationParameter!.Value;
int colorPlaneIndex = this.sequenceParameterSet.SeparateColorPlaneFlag ? independentSlice.ColorPlaneId : 0;
this.lastCodedQuantizationParameter = sliceQuantizationParameter;
this.currentQuantizationParameter = sliceQuantizationParameter;
this.currentChromaQuantizationAdjustment = 0;
this.currentSliceChromaBlueQuantizationOffset = independentSlice.ChromaCbQuantizationParameterOffset;
this.currentSliceChromaRedQuantizationOffset = independentSlice.ChromaCrQuantizationParameterOffset;
this.quantizationParameterDeltaPending = this.pictureParameterSet.CodingUnitQuantizationParameterDeltaEnabled;
this.chromaQuantizationAdjustmentPending = independentSlice.ChromaQuantizationParameterOffsetListEnabled == true;
int substreamIndex = 0;
HevcCabacSyntaxReader reader = new(slice.GetEntropySubstream(substreamIndex).Span, sliceQuantizationParameter);
this.coefficientDecoder.ResetRiceAdaptation();
int contextOffset = colorPlaneIndex * HevcCabacContexts.ContextCount;
int riceOffset = colorPlaneIndex * 4;
int startRasterAddress = tileLayout.GetRasterAddress(startAddressInTileScan);
tileLayout.GetTilePosition(
startRasterAddress,
out int startTileIndex,
out int startColumnInTile,
out int startRowInTile,
out int startTileWidth,
out _);
bool startsAtTileOrigin = startColumnInTile == 0 && startRowInTile == 0;
bool canInheritSliceSegmentContexts = !startsAtTileOrigin
&& (startTileWidth >= 2 || !this.pictureParameterSet.EntropyCodingSynchronizationEnabled);
// A dependent segment normally resumes the preceding CABAC state. Tile origins and one-CTB-wide WPP
// rows are initialization boundaries instead, matching the availability rules used by the reference decoder.
if (slice.DependentSliceSegment
&& canInheritSliceSegmentContexts
&& this.hasSliceSegmentContexts[colorPlaneIndex])
{
reader.CopyContextsFrom(this.sliceSegmentContexts.AsSpan(contextOffset, HevcCabacContexts.ContextCount));
this.coefficientDecoder.CopyRiceAdaptationFrom(this.sliceSegmentRiceAdaptation.AsSpan(riceOffset, 4));
}
if (!slice.DependentSliceSegment)
{
// An independent slice starts a new prediction region, so an upper-right CTB from the preceding
// independent slice cannot supply wavefront contexts to its first row.
this.hasWavefrontContexts[colorPlaneIndex] = false;
}
bool startsAtWavefrontRow = this.pictureParameterSet.EntropyCodingSynchronizationEnabled
&& startColumnInTile == 0
&& startRowInTile > 0;
if (startsAtWavefrontRow
&& startTileWidth > 1
&& this.hasWavefrontContexts[colorPlaneIndex]
&& this.wavefrontContextTileIndices[colorPlaneIndex] == startTileIndex)
{
// A dependent segment can begin exactly at a wavefront row boundary. Its first substream still uses
// the upper-right state captured from the preceding row; no substream transition occurs inside this call.
reader.CopyContextsFrom(this.wavefrontContexts.AsSpan(contextOffset, HevcCabacContexts.ContextCount));
this.coefficientDecoder.CopyRiceAdaptationFrom(this.wavefrontRiceAdaptation.AsSpan(riceOffset, 4));
}
int codingTreeBlockSize = 1 << this.sequenceParameterSet.CodingTreeBlockLog2;
int tileScanAddress = startAddressInTileScan;
bool firstCodingTreeBlock = true;
while (tileScanAddress < tileLayout.Width * tileLayout.Height)
{
int rasterAddress = tileLayout.GetRasterAddress(tileScanAddress);
tileLayout.GetTilePosition(
rasterAddress,
out int tileIndex,
out int columnInTile,
out int rowInTile,
out int tileWidth,
out int tileHeight);
bool startsTile = columnInTile == 0 && rowInTile == 0;
bool startsWavefrontRow = this.pictureParameterSet.EntropyCodingSynchronizationEnabled && columnInTile == 0 && rowInTile > 0;
if (!firstCodingTreeBlock && (startsTile || startsWavefrontRow))
{
if (!reader.ReadTerminate())
{
throw new InvalidImageContentException("The HEVC entropy substream does not terminate at its tile or wavefront boundary.");
}
reader.ValidateTerminationAlignment();
substreamIndex++;
if (substreamIndex >= slice.EntropySubstreamCount)
{
throw new InvalidImageContentException("The HEVC slice segment has too few entropy entry points.");
}
reader = new HevcCabacSyntaxReader(slice.GetEntropySubstream(substreamIndex).Span, sliceQuantizationParameter);
this.coefficientDecoder.ResetRiceAdaptation();
this.lastCodedQuantizationParameter = sliceQuantizationParameter;
if (startsWavefrontRow
&& tileWidth > 1
&& this.hasWavefrontContexts[colorPlaneIndex]
&& this.wavefrontContextTileIndices[colorPlaneIndex] == tileIndex)
{
reader.CopyContextsFrom(this.wavefrontContexts.AsSpan(contextOffset, HevcCabacContexts.ContextCount));
this.coefficientDecoder.CopyRiceAdaptationFrom(this.wavefrontRiceAdaptation.AsSpan(riceOffset, 4));
}
}
int ctbX = rasterAddress % tileLayout.Width;
int ctbY = rasterAddress / tileLayout.Width;
int x = ctbX * codingTreeBlockSize;
int y = ctbY * codingTreeBlockSize;
int regionId = ((independentSliceIndex - 1) * tileLayout.TileCount) + tileIndex + 1;
HevcPlane regionPlane = this.sequenceParameterSet.SeparateColorPlaneFlag ? (HevcPlane)colorPlaneIndex : HevcPlane.Y;
HevcLoopFilterRegion loopFilterRegion = new(
independentSliceStartAddressInTileScan,
tileIndex,
independentSlice.LoopFilterAcrossSlicesEnabled == true,
independentSlice.DeblockingFilterDisabled == true,
independentSlice.DeblockingFilterBetaOffsetDiv2,
independentSlice.DeblockingFilterTcOffsetDiv2);
this.sampleAdaptiveOffsetState.SetLoopFilterRegion(rasterAddress, regionPlane, loopFilterRegion);
this.DecodeSampleAdaptiveOffset(ref reader, independentSlice, rasterAddress, ctbX, ctbY, regionId);
this.DecodeCodingTree(
ref reader,
x,
y,
this.sequenceParameterSet.CodingTreeBlockLog2,
0,
regionId,
colorPlaneIndex);
// HEVC places end_of_slice_segment_flag after the final coding unit of each complete CTB. Reading it
// inside the recursive leaf traversal consumes coefficient data whenever a CTB contains multiple CUs.
bool endOfSliceSegment = reader.ReadTerminate();
// Wavefront synchronization copies probability and persistent Rice state after the second CTB of each
// row. The next row starts with those contexts but a newly initialized arithmetic register.
if (this.pictureParameterSet.EntropyCodingSynchronizationEnabled && columnInTile == 1)
{
reader.CopyContextsTo(this.wavefrontContexts.AsSpan(contextOffset, HevcCabacContexts.ContextCount));
this.coefficientDecoder.CopyRiceAdaptationTo(this.wavefrontRiceAdaptation.AsSpan(riceOffset, 4));
this.hasWavefrontContexts[colorPlaneIndex] = true;
this.wavefrontContextTileIndices[colorPlaneIndex] = tileIndex;
}
tileScanAddress++;
firstCodingTreeBlock = false;
if (endOfSliceSegment)
{
reader.ValidateTerminationAlignment();
if (substreamIndex + 1 != slice.EntropySubstreamCount)
{
throw new InvalidImageContentException("The HEVC slice segment has unused entropy entry points.");
}
reader.CopyContextsTo(this.sliceSegmentContexts.AsSpan(contextOffset, HevcCabacContexts.ContextCount));
this.coefficientDecoder.CopyRiceAdaptationTo(this.sliceSegmentRiceAdaptation.AsSpan(riceOffset, 4));
this.hasSliceSegmentContexts[colorPlaneIndex] = true;
return tileScanAddress;
}
bool atTileEnd = columnInTile == tileWidth - 1 && rowInTile == tileHeight - 1;
bool atWavefrontRowEnd = this.pictureParameterSet.EntropyCodingSynchronizationEnabled && columnInTile == tileWidth - 1;
if (atTileEnd || atWavefrontRowEnd)
{
// A non-final tile or wavefront row has a second terminating bin after the coding-unit end flag.
// It is consumed when the following loop iteration opens the next bounded entropy substream.
continue;
}
}
throw new InvalidImageContentException("The HEVC slice segment reaches the picture boundary without termination.");
}
/// <summary>
/// Decodes one coding-tree node in depth-first Z order.
/// </summary>
/// <param name="reader">The active entropy-substream reader.</param>
/// <param name="x">The coding-node left luma coordinate.</param>
/// <param name="y">The coding-node top luma coordinate.</param>
/// <param name="log2Size">The base-two logarithm of the coding-node side.</param>
/// <param name="depth">The coding-tree depth below the coding-tree-block root.</param>
/// <param name="regionId">The current independent-slice and tile prediction region.</param>
/// <param name="colorPlaneIndex">The selected separate-color plane, or zero for combined coding.</param>
private void DecodeCodingTree(
ref HevcCabacSyntaxReader reader,
int x,
int y,
int log2Size,
int depth,
int regionId,
int colorPlaneIndex)
{
int size = 1 << log2Size;
bool crossesPictureBoundary = x + size > this.sequenceParameterSet.Width || y + size > this.sequenceParameterSet.Height;
bool canSplit = log2Size > this.sequenceParameterSet.MinCodingBlockLog2;
HevcCodingTreeState codingTreeState = this.codingTreeStates[colorPlaneIndex];
bool split = false;
if (canSplit)
{
if (crossesPictureBoundary)
{
split = true;
}
else
{
bool leftAvailable = this.reconstructionState.IsReconstructed((HevcPlane)colorPlaneIndex, x - 1, y, regionId);
bool aboveAvailable = this.reconstructionState.IsReconstructed((HevcPlane)colorPlaneIndex, x, y - 1, regionId);
int context = codingTreeState.GetSplitContext(x, y, depth, leftAvailable, aboveAvailable);
split = reader.ReadSplit(context);
}
}
bool startsQuantizationGroup = depth == this.pictureParameterSet.QuantizationParameterDeltaDepth
|| (!split && depth < this.pictureParameterSet.QuantizationParameterDeltaDepth);
if (startsQuantizationGroup && this.pictureParameterSet.CodingUnitQuantizationParameterDeltaEnabled)
{
// A leaf above the configured QG depth owns one complete quantization group. Waiting for the configured
// depth would carry the preceding group's coded-delta state into this coding unit and skip required syntax.
this.BeginQuantizationGroup(x, y, regionId, colorPlaneIndex);
}
bool startsChromaQuantizationGroup = depth == this.pictureParameterSet.ChromaQuantizationParameterOffsetDepth
|| (!split && depth < this.pictureParameterSet.ChromaQuantizationParameterOffsetDepth);
if (startsChromaQuantizationGroup && this.pictureParameterSet.ChromaQuantizationParameterOffsetsCb.Count != 0)
{
this.currentChromaQuantizationAdjustment = 0;
this.chromaQuantizationAdjustmentPending = true;
}
if (split)
{
int childLog2Size = log2Size - 1;
int childSize = 1 << childLog2Size;
for (int child = 0; child < 4; child++)
{
int childX = x + ((child & 1) * childSize);
int childY = y + ((child >> 1) * childSize);
if (childX >= this.sequenceParameterSet.Width || childY >= this.sequenceParameterSet.Height)
{
continue;
}
this.DecodeCodingTree(
ref reader,
childX,
childY,
childLog2Size,
depth + 1,
regionId,
colorPlaneIndex);
}
return;
}
this.DecodeCodingUnit(ref reader, x, y, log2Size, depth, regionId, colorPlaneIndex);
}
/// <summary>
/// Decodes and reconstructs one intra-coded leaf coding unit.
/// </summary>
/// <param name="reader">The active entropy-substream reader.</param>
/// <param name="x">The coding-unit left luma coordinate.</param>
/// <param name="y">The coding-unit top luma coordinate.</param>
/// <param name="log2Size">The base-two logarithm of the coding-unit side.</param>
/// <param name="depth">The coding-tree depth.</param>
/// <param name="regionId">The current independent-slice and tile prediction region.</param>
/// <param name="colorPlaneIndex">The selected separate-color plane, or zero for combined coding.</param>
private void DecodeCodingUnit(
ref HevcCabacSyntaxReader reader,
int x,
int y,
int log2Size,
int depth,
int regionId,
int colorPlaneIndex)
{
bool transquantBypass = this.pictureParameterSet.TransquantizationBypassEnabled && reader.ReadTransquantBypass();
bool usesNxNPartitions = reader.ReadIntraNxNPartition(log2Size == this.sequenceParameterSet.MinCodingBlockLog2);
HevcPlane primaryPlane = this.sequenceParameterSet.SeparateColorPlaneFlag ? (HevcPlane)colorPlaneIndex : HevcPlane.Y;
bool pcm = this.sequenceParameterSet.PcmEnabled
&& !usesNxNPartitions
&& log2Size >= this.sequenceParameterSet.MinPcmCodingBlockLog2
&& log2Size <= this.sequenceParameterSet.MaxPcmCodingBlockLog2
&& reader.ReadPcmFlag();
if (pcm)
{
int size = 1 << log2Size;
this.deblockingState.MarkBlock(primaryPlane, x, y, size, size);
this.DecodePcmCodingUnit(ref reader, x, y, log2Size, regionId, colorPlaneIndex);
reader.RestartAfterPcm();
}
else
{
HevcIntraPredictionState predictionState = this.intraPredictionStates[colorPlaneIndex];
HevcPlane boundaryPlane = this.sequenceParameterSet.SeparateColorPlaneFlag ? (HevcPlane)colorPlaneIndex : HevcPlane.Y;
bool leftAvailable = this.reconstructionState.IsReconstructed(boundaryPlane, x - 1, y, regionId);
bool aboveAvailable = this.reconstructionState.IsReconstructed(boundaryPlane, x, y - 1, regionId);
predictionState.DecodeLumaModes(ref reader, x, y, log2Size, usesNxNPartitions, leftAvailable, aboveAvailable);
if (this.sequenceParameterSet.ChromaFormat != 0 && !this.sequenceParameterSet.SeparateColorPlaneFlag)
{
predictionState.DecodeChromaModes(ref reader, x, y, log2Size, usesNxNPartitions);
}
int minimumTransformLog2 = GetMinimumTransformLog2Size(this.sequenceParameterSet, log2Size, usesNxNPartitions);
HevcTransformUnitGeometry geometry = HevcTransformUnitGeometry.CreateRoot(
x,
y,
log2Size,
this.sequenceParameterSet.ChromaFormat,
this.sequenceParameterSet.SeparateColorPlaneFlag,
colorPlaneIndex);
this.DecodeTransformTree(
ref reader,
in geometry,
0,
minimumTransformLog2,
usesNxNPartitions,
transquantBypass,
regionId,
colorPlaneIndex,
default,
default);
}
this.codingTreeStates[colorPlaneIndex].SetCodingUnit(
x,
y,
log2Size,
depth,
this.currentQuantizationParameter,
transquantBypass,
pcm);
this.lastCodedQuantizationParameter = this.currentQuantizationParameter;
}
/// <summary>
/// Begins one luma quantization group using available spatial predictors.
/// </summary>
/// <param name="x">The quantization-group left luma coordinate.</param>
/// <param name="y">The quantization-group top luma coordinate.</param>
/// <param name="regionId">The current independent-slice and tile prediction region.</param>
/// <param name="colorPlaneIndex">The selected separate-color plane, or zero for combined coding.</param>
private void BeginQuantizationGroup(int x, int y, int regionId, int colorPlaneIndex)
{
HevcPlane plane = this.sequenceParameterSet.SeparateColorPlaneFlag ? (HevcPlane)colorPlaneIndex : HevcPlane.Y;
int codingTreeBlockMask = (1 << this.sequenceParameterSet.CodingTreeBlockLog2) - 1;
// QP prediction neighbours are confined to the current CTB. This differs from intra sample availability,
// which may legitimately use reconstructed samples across the same left or upper CTB boundary.
bool leftAvailable = (x & codingTreeBlockMask) != 0 && this.reconstructionState.IsReconstructed(plane, x - 1, y, regionId);
bool aboveAvailable = (y & codingTreeBlockMask) != 0 && this.reconstructionState.IsReconstructed(plane, x, y - 1, regionId);
int fallback = this.lastCodedQuantizationParameter;
HevcCodingTreeState codingTreeState = this.codingTreeStates[colorPlaneIndex];
int left = leftAvailable ? codingTreeState.GetQuantizationParameter(x - 1, y) : fallback;
int above = aboveAvailable ? codingTreeState.GetQuantizationParameter(x, y - 1) : fallback;
this.currentQuantizationParameter = (left + above + 1) >> 1;
this.quantizationParameterDeltaPending = true;
}
/// <summary>
/// Derives the smallest luma transform permitted within one intra coding unit.
/// </summary>
/// <param name="sequenceParameterSet">The transform hierarchy limits.</param>
/// <param name="codingUnitLog2Size">The base-two logarithm of the coding-unit side.</param>
/// <param name="usesNxNPartitions">Whether the coding unit has four luma prediction partitions.</param>
/// <returns>The minimum luma transform side as a base-two logarithm.</returns>
private static int GetMinimumTransformLog2Size(
HevcSequenceParameterSet sequenceParameterSet,
int codingUnitLog2Size,
bool usesNxNPartitions)
{
int hierarchyReduction = sequenceParameterSet.MaxTransformHierarchyDepthIntra - 1 + (usesNxNPartitions ? 1 : 0);
int minimum = codingUnitLog2Size < sequenceParameterSet.MinTransformBlockLog2 + hierarchyReduction
? sequenceParameterSet.MinTransformBlockLog2
: codingUnitLog2Size - hierarchyReduction;
return Math.Min(minimum, sequenceParameterSet.MaxTransformBlockLog2);
}
}

363
src/ImageSharp/Formats/Heif/Hevc/HevcPictureDecoder.cs

@ -1,363 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Owns the bounded state used to reconstruct one independently decodable HEVC still picture.
/// </summary>
internal sealed partial class HevcPictureDecoder : IDisposable
{
/// <summary>
/// The maximum square transform-block sample count.
/// </summary>
private const int MaximumTransformSampleCount = 32 * 32;
/// <summary>
/// The largest reference array used by a thirty-two-sample prediction block.
/// </summary>
private const int MaximumReferenceLength = (2 * 32) + 1;
/// <summary>
/// The configuration providing picture-lifetime allocations.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// The active picture parameters.
/// </summary>
private readonly HevcPictureParameterSet pictureParameterSet;
/// <summary>
/// The active sequence parameters.
/// </summary>
private readonly HevcSequenceParameterSet sequenceParameterSet;
/// <summary>
/// The decoded coding-unit state.
/// </summary>
private readonly HevcCodingTreeState[] codingTreeStates;
/// <summary>
/// The decoded intra-prediction modes.
/// </summary>
private readonly HevcIntraPredictionState[] intraPredictionStates;
/// <summary>
/// The completed prediction-block state used for reference availability.
/// </summary>
private readonly HevcReconstructionState reconstructionState;
/// <summary>
/// The reusable coefficient entropy decoder.
/// </summary>
private readonly HevcCoefficientDecoder coefficientDecoder;
/// <summary>
/// The resolved sample-adaptive-offset parameters for every coding-tree block.
/// </summary>
private readonly HevcSampleAdaptiveOffsetState sampleAdaptiveOffsetState;
/// <summary>
/// The transform and prediction boundaries required by the deblocking stage.
/// </summary>
private readonly HevcDeblockingState deblockingState;
/// <summary>
/// The integer coefficient, residual, and transform workspace.
/// </summary>
private readonly IMemoryOwner<int> integerScratch;
/// <summary>
/// The prediction, reference, and reference-substitution workspace.
/// </summary>
private readonly IMemoryOwner<ushort> predictionScratch;
/// <summary>
/// The ordered intra-reference availability workspace.
/// </summary>
private readonly IMemoryOwner<bool> availabilityScratch;
/// <summary>
/// The per-color-plane adaptive contexts captured after the second coding-tree block of a wavefront row.
/// </summary>
private readonly HevcCabacContext[] wavefrontContexts = new HevcCabacContext[HevcCabacContexts.ContextCount * 3];
/// <summary>
/// The per-color-plane persistent Rice statistics captured with the wavefront probability contexts.
/// </summary>
private readonly int[] wavefrontRiceAdaptation = new int[12];
/// <summary>
/// Whether retained wavefront contexts are available for each color plane.
/// </summary>
private InlineArray4<bool> hasWavefrontContexts;
/// <summary>
/// The tile that owns each color plane's retained wavefront contexts.
/// </summary>
private InlineArray4<int> wavefrontContextTileIndices;
/// <summary>
/// The adaptive contexts retained at the end of a dependent-slice prediction region.
/// </summary>
private readonly HevcCabacContext[] sliceSegmentContexts = new HevcCabacContext[HevcCabacContexts.ContextCount * 3];
/// <summary>
/// The persistent Rice statistics retained with dependent-slice probability contexts.
/// </summary>
private readonly int[] sliceSegmentRiceAdaptation = new int[12];
/// <summary>
/// Whether retained dependent-slice contexts are available.
/// </summary>
private InlineArray4<bool> hasSliceSegmentContexts;
/// <summary>
/// The luma quantization parameter most recently coded in the current prediction region.
/// </summary>
private int lastCodedQuantizationParameter;
/// <summary>
/// The effective luma quantization parameter of the current quantization group.
/// </summary>
private int currentQuantizationParameter;
/// <summary>
/// The one-based chroma quantization-offset-list selector of the current quantization group.
/// </summary>
private int currentChromaQuantizationAdjustment;
/// <summary>
/// The Cb quantization-parameter offset signaled by the governing independent slice.
/// </summary>
private int currentSliceChromaBlueQuantizationOffset;
/// <summary>
/// The Cr quantization-parameter offset signaled by the governing independent slice.
/// </summary>
private int currentSliceChromaRedQuantizationOffset;
/// <summary>
/// Whether the current quantization group can still signal its luma delta.
/// </summary>
private bool quantizationParameterDeltaPending;
/// <summary>
/// Whether the current quantization group can still signal its chroma adjustment.
/// </summary>
private bool chromaQuantizationAdjustmentPending;
/// <summary>
/// Initializes a new instance of the <see cref="HevcPictureDecoder"/> class.
/// </summary>
/// <param name="configuration">The configuration providing all decoder-owned memory.</param>
/// <param name="pictureParameterSet">The picture parameters governing the coded still image.</param>
public HevcPictureDecoder(Configuration configuration, HevcPictureParameterSet pictureParameterSet)
{
this.configuration = configuration;
this.pictureParameterSet = pictureParameterSet;
this.sequenceParameterSet = pictureParameterSet.SequenceParameterSet;
HevcPictureBuffer? picture = null;
HevcCodingTreeState[]? codingTreeStates = null;
HevcIntraPredictionState[]? intraPredictionStates = null;
HevcReconstructionState? reconstructionState = null;
HevcCoefficientDecoder? coefficientDecoder = null;
HevcSampleAdaptiveOffsetState? sampleAdaptiveOffsetState = null;
HevcDeblockingState? deblockingState = null;
IMemoryOwner<int>? integerScratch = null;
IMemoryOwner<ushort>? predictionScratch = null;
IMemoryOwner<bool>? availabilityScratch = null;
try
{
picture = new HevcPictureBuffer(configuration, this.sequenceParameterSet);
int codingTreeStateCount = this.sequenceParameterSet.SeparateColorPlaneFlag ? 3 : 1;
codingTreeStates = new HevcCodingTreeState[codingTreeStateCount];
for (int index = 0; index < codingTreeStates.Length; index++)
{
codingTreeStates[index] = new HevcCodingTreeState(configuration, this.sequenceParameterSet);
}
int intraPredictionStateCount = this.sequenceParameterSet.SeparateColorPlaneFlag ? 3 : 1;
intraPredictionStates = new HevcIntraPredictionState[intraPredictionStateCount];
for (int index = 0; index < intraPredictionStates.Length; index++)
{
intraPredictionStates[index] = new HevcIntraPredictionState(configuration, this.sequenceParameterSet);
}
reconstructionState = new HevcReconstructionState(configuration, this.sequenceParameterSet);
coefficientDecoder = new HevcCoefficientDecoder(configuration);
int codingTreeBlockCount = HevcParameterSetSyntax.GetCodingTreeBlockCount(
this.sequenceParameterSet.Width,
this.sequenceParameterSet.CodingTreeBlockLog2)
* HevcParameterSetSyntax.GetCodingTreeBlockCount(
this.sequenceParameterSet.Height,
this.sequenceParameterSet.CodingTreeBlockLog2);
sampleAdaptiveOffsetState = new HevcSampleAdaptiveOffsetState(configuration, codingTreeBlockCount);
deblockingState = new HevcDeblockingState(configuration, this.sequenceParameterSet);
// Six transform-sized integer regions retain quantized, dequantized, reconstructed, cross-component, and
// two-pass inverse-transform data without allocating in coding-unit or transform-unit loops.
integerScratch = configuration.MemoryAllocator.Allocate<int>(MaximumTransformSampleCount * 6);
int maximumPredictionScratch = HevcIntraPredictor.GetScratchLength(5);
int maximumReferenceScratch = HevcIntraPredictor.GetReferenceScratchLength(5, 4);
predictionScratch = configuration.MemoryAllocator.Allocate<ushort>(
MaximumTransformSampleCount + maximumPredictionScratch + maximumReferenceScratch + (MaximumReferenceLength * 4));
availabilityScratch = configuration.MemoryAllocator.Allocate<bool>((4 * 32 / 2) + 1);
this.Picture = picture;
this.codingTreeStates = codingTreeStates;
this.intraPredictionStates = intraPredictionStates;
this.reconstructionState = reconstructionState;
this.coefficientDecoder = coefficientDecoder;
this.sampleAdaptiveOffsetState = sampleAdaptiveOffsetState;
this.deblockingState = deblockingState;
this.integerScratch = integerScratch;
this.predictionScratch = predictionScratch;
this.availabilityScratch = availabilityScratch;
}
catch
{
// No decoder ownership is published when construction fails. Unwind every completed child owner in reverse
// order because the caller cannot dispose an object whose constructor did not return.
availabilityScratch?.Dispose();
predictionScratch?.Dispose();
integerScratch?.Dispose();
deblockingState?.Dispose();
sampleAdaptiveOffsetState?.Dispose();
coefficientDecoder?.Dispose();
reconstructionState?.Dispose();
if (intraPredictionStates is not null)
{
for (int index = intraPredictionStates.Length - 1; index >= 0; index--)
{
intraPredictionStates[index]?.Dispose();
}
}
if (codingTreeStates is not null)
{
for (int index = codingTreeStates.Length - 1; index >= 0; index--)
{
codingTreeStates[index]?.Dispose();
}
}
picture?.Dispose();
throw;
}
}
/// <summary>
/// Gets the native-precision reconstructed component planes.
/// </summary>
public HevcPictureBuffer Picture { get; }
/// <summary>
/// Reconstructs every ordered slice segment in one independently decodable image item.
/// </summary>
/// <param name="bitstream">The validated image-item NAL units and slice segments.</param>
/// <exception cref="InvalidImageContentException">
/// A slice changes the coded picture parameters, overlaps an earlier segment, or does not terminate at a valid
/// coding-tree boundary.
/// </exception>
public void Decode(HevcImageItemBitstream bitstream)
{
HevcTileLayout tileLayout = new(this.pictureParameterSet);
int planeCount = this.sequenceParameterSet.SeparateColorPlaneFlag ? 3 : 1;
int[] nextCodingTreeBlockAddressesInTileScan = new int[planeCount];
int[] independentSliceIndices = new int[planeCount];
HevcSliceSegmentHeader?[] independentSlices = new HevcSliceSegmentHeader?[planeCount];
for (int sliceIndex = 0; sliceIndex < bitstream.SliceSegments.Count; sliceIndex++)
{
HevcSliceSegmentHeader slice = bitstream.SliceSegments[sliceIndex];
int colorPlane = this.sequenceParameterSet.SeparateColorPlaneFlag ? slice.ColorPlaneId : 0;
if (slice.PictureParameterSet.Id != this.pictureParameterSet.Id
|| slice.PictureParameterSet.SequenceParameterSetId != this.pictureParameterSet.SequenceParameterSetId)
{
throw new InvalidImageContentException("The HEVC still picture changes parameter sets between slice segments.");
}
if (!slice.DependentSliceSegment)
{
independentSlices[colorPlane] = slice;
independentSliceIndices[colorPlane]++;
}
HevcSliceSegmentHeader? independentSlice = independentSlices[colorPlane];
if (independentSlice is null)
{
throw new InvalidImageContentException("The HEVC still picture begins with a dependent slice segment.");
}
int sliceStartAddressInTileScan = tileLayout.GetTileScanAddress(slice.SliceSegmentAddress);
if (sliceStartAddressInTileScan != nextCodingTreeBlockAddressesInTileScan[colorPlane])
{
throw new InvalidImageContentException("The HEVC slice segments do not cover the coded picture in order.");
}
nextCodingTreeBlockAddressesInTileScan[colorPlane] = this.DecodeSliceSegment(
slice,
independentSlice,
independentSliceIndices[colorPlane],
in tileLayout,
sliceStartAddressInTileScan,
tileLayout.GetTileScanAddress(independentSlice.SliceSegmentAddress));
}
int codingTreeBlockCount = HevcParameterSetSyntax.GetCodingTreeBlockCount(
this.sequenceParameterSet.Width,
this.sequenceParameterSet.CodingTreeBlockLog2)
* HevcParameterSetSyntax.GetCodingTreeBlockCount(
this.sequenceParameterSet.Height,
this.sequenceParameterSet.CodingTreeBlockLog2);
foreach (int nextAddress in nextCodingTreeBlockAddressesInTileScan)
{
if (nextAddress != codingTreeBlockCount)
{
throw new InvalidImageContentException("The HEVC slice segments do not reconstruct the complete coded picture.");
}
}
this.ApplyDeblockingFilter(in tileLayout);
if (this.sampleAdaptiveOffsetState.HasEnabledParameters)
{
// SAO classification always observes the complete post-deblocking picture, never samples already offset by an
// earlier CTB. One picture-lifetime snapshot provides that invariant without row allocations or filter-order coupling.
using HevcPictureBuffer sampleAdaptiveOffsetSource = new(this.configuration, this.sequenceParameterSet);
this.Picture.CopyTo(sampleAdaptiveOffsetSource);
this.ApplySampleAdaptiveOffset(sampleAdaptiveOffsetSource, in tileLayout);
}
}
/// <summary>
/// Releases all current-picture state and reconstructed planes.
/// </summary>
public void Dispose()
{
this.availabilityScratch.Dispose();
this.predictionScratch.Dispose();
this.integerScratch.Dispose();
this.deblockingState.Dispose();
this.sampleAdaptiveOffsetState.Dispose();
this.coefficientDecoder.Dispose();
this.reconstructionState.Dispose();
foreach (HevcIntraPredictionState state in this.intraPredictionStates)
{
state.Dispose();
}
foreach (HevcCodingTreeState state in this.codingTreeStates)
{
state.Dispose();
}
this.Picture.Dispose();
}
}

558
src/ImageSharp/Formats/Heif/Hevc/HevcPictureParameterSet.cs

@ -1,558 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Contains the HEVC picture fields required to decode the independently coded picture in one still-image item.
/// </summary>
internal sealed class HevcPictureParameterSet
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcPictureParameterSet"/> class.
/// </summary>
/// <param name="nalUnit">The decoded picture-parameter-set NAL unit.</param>
/// <param name="sequenceParameterSets">The sequence parameter sets available to the coded image item.</param>
/// <exception cref="InvalidImageContentException">
/// The picture parameter set is malformed, references an unavailable sequence parameter set, or declares
/// picture geometry outside that sequence parameter set.
/// </exception>
public HevcPictureParameterSet(
HevcNalUnit nalUnit,
IReadOnlyList<HevcSequenceParameterSet> sequenceParameterSets)
{
const byte pictureParameterSetNalUnitType = 34;
if (nalUnit.Header.NalUnitType != pictureParameterSetNalUnitType
|| nalUnit.Header.LayerId != 0
|| nalUnit.Header.TemporalId != 0)
{
throw new InvalidImageContentException("The HEVC picture parameter set has an invalid NAL-unit header.");
}
HevcBitReader reader = new(nalUnit.Rbsp.Span);
uint pictureParameterSetId = reader.ReadUnsignedExpGolomb();
uint sequenceParameterSetId = reader.ReadUnsignedExpGolomb();
if (pictureParameterSetId > 63 || sequenceParameterSetId > 15)
{
throw new InvalidImageContentException("The HEVC picture parameter set has an invalid identifier.");
}
this.Id = (byte)pictureParameterSetId;
this.SequenceParameterSetId = (byte)sequenceParameterSetId;
HevcSequenceParameterSet? sequenceParameterSet = null;
foreach (HevcSequenceParameterSet candidate in sequenceParameterSets)
{
if (candidate.Id == this.SequenceParameterSetId)
{
sequenceParameterSet = candidate;
break;
}
}
if (sequenceParameterSet is null)
{
throw new InvalidImageContentException("The HEVC picture parameter set references an unavailable sequence parameter set.");
}
this.SequenceParameterSet = sequenceParameterSet;
this.DependentSliceSegmentsEnabled = reader.ReadFlag();
this.OutputFlagPresent = reader.ReadFlag();
this.ExtraSliceHeaderBitCount = (int)reader.ReadBits(3);
this.SignDataHidingEnabled = reader.ReadFlag();
this.CabacInitializationPresent = reader.ReadFlag();
uint defaultReferenceIndexCountList0MinusOne = reader.ReadUnsignedExpGolomb();
uint defaultReferenceIndexCountList1MinusOne = reader.ReadUnsignedExpGolomb();
if (defaultReferenceIndexCountList0MinusOne > 14 || defaultReferenceIndexCountList1MinusOne > 14)
{
throw new InvalidImageContentException("The HEVC picture parameter set declares too many default reference indices.");
}
this.DefaultReferenceIndexCountList0 = (int)defaultReferenceIndexCountList0MinusOne + 1;
this.DefaultReferenceIndexCountList1 = (int)defaultReferenceIndexCountList1MinusOne + 1;
this.InitialQuantizationParameterMinus26 = reader.ReadSignedExpGolomb();
int minimumInitialQuantizationParameter = -26 - (6 * (sequenceParameterSet.BitDepthLuma - 8));
if (this.InitialQuantizationParameterMinus26 < minimumInitialQuantizationParameter
|| this.InitialQuantizationParameterMinus26 > 25)
{
throw new InvalidImageContentException("The HEVC picture parameter set has an invalid initial quantization parameter.");
}
this.ConstrainedIntraPredictionEnabled = reader.ReadFlag();
this.TransformSkipEnabled = reader.ReadFlag();
this.CodingUnitQuantizationParameterDeltaEnabled = reader.ReadFlag();
if (this.CodingUnitQuantizationParameterDeltaEnabled)
{
uint quantizationParameterDeltaDepth = reader.ReadUnsignedExpGolomb();
int maximumDepth = sequenceParameterSet.CodingTreeBlockLog2 - sequenceParameterSet.MinCodingBlockLog2;
if (quantizationParameterDeltaDepth > maximumDepth)
{
throw new InvalidImageContentException("The HEVC picture parameter set has an invalid quantization-parameter delta depth.");
}
this.QuantizationParameterDeltaDepth = (int)quantizationParameterDeltaDepth;
}
this.ChromaCbQuantizationParameterOffset = HevcParameterSetSyntax.ReadQuantizationParameterOffset(ref reader);
this.ChromaCrQuantizationParameterOffset = HevcParameterSetSyntax.ReadQuantizationParameterOffset(ref reader);
this.SliceChromaQuantizationParameterOffsetsPresent = reader.ReadFlag();
this.WeightedPredictionEnabled = reader.ReadFlag();
this.WeightedBiPredictionEnabled = reader.ReadFlag();
this.TransquantizationBypassEnabled = reader.ReadFlag();
this.TilesEnabled = reader.ReadFlag();
this.EntropyCodingSynchronizationEnabled = reader.ReadFlag();
int codingTreeBlockColumns = HevcParameterSetSyntax.GetCodingTreeBlockCount(
sequenceParameterSet.Width,
sequenceParameterSet.CodingTreeBlockLog2);
int codingTreeBlockRows = HevcParameterSetSyntax.GetCodingTreeBlockCount(
sequenceParameterSet.Height,
sequenceParameterSet.CodingTreeBlockLog2);
if (this.TilesEnabled)
{
uint tileColumnCountMinusOne = reader.ReadUnsignedExpGolomb();
uint tileRowCountMinusOne = reader.ReadUnsignedExpGolomb();
if (tileColumnCountMinusOne >= codingTreeBlockColumns
|| tileRowCountMinusOne >= codingTreeBlockRows
|| (tileColumnCountMinusOne == 0 && tileRowCountMinusOne == 0))
{
throw new InvalidImageContentException("The HEVC picture parameter set has an invalid tile grid.");
}
int tileColumnCount = (int)tileColumnCountMinusOne + 1;
int tileRowCount = (int)tileRowCountMinusOne + 1;
this.UniformTileSpacing = reader.ReadFlag();
this.TileColumnWidths = ReadTileDimensions(
ref reader,
codingTreeBlockColumns,
tileColumnCount,
this.UniformTileSpacing);
this.TileRowHeights = ReadTileDimensions(
ref reader,
codingTreeBlockRows,
tileRowCount,
this.UniformTileSpacing);
this.LoopFilterAcrossTilesEnabled = reader.ReadFlag();
}
else
{
// A picture without tile syntax is one tile spanning the coded CTB grid. Materializing that inferred
// layout lets slice addressing use the same bounded arrays for tiled and untiled image items.
this.UniformTileSpacing = true;
this.TileColumnWidths = [codingTreeBlockColumns];
this.TileRowHeights = [codingTreeBlockRows];
this.LoopFilterAcrossTilesEnabled = true;
}
this.LoopFilterAcrossSlicesEnabled = reader.ReadFlag();
this.DeblockingFilterControlPresent = reader.ReadFlag();
if (this.DeblockingFilterControlPresent)
{
this.DeblockingFilterOverrideEnabled = reader.ReadFlag();
this.DeblockingFilterDisabled = reader.ReadFlag();
if (!this.DeblockingFilterDisabled)
{
this.DeblockingFilterBetaOffsetDiv2 = HevcParameterSetSyntax.ReadDeblockingFilterOffset(ref reader);
this.DeblockingFilterTcOffsetDiv2 = HevcParameterSetSyntax.ReadDeblockingFilterOffset(ref reader);
}
}
this.ScalingListDataPresent = reader.ReadFlag();
if (this.ScalingListDataPresent && !sequenceParameterSet.ScalingListEnabled)
{
throw new InvalidImageContentException("The HEVC picture parameter set declares scaling data disabled by its sequence parameter set.");
}
this.ScalingList = this.ScalingListDataPresent
? HevcScalingList.Parse(ref reader)
: sequenceParameterSet.ScalingList;
this.ReferenceListModificationPresent = reader.ReadFlag();
uint parallelMergeLevelMinusTwo = reader.ReadUnsignedExpGolomb();
if (parallelMergeLevelMinusTwo > sequenceParameterSet.CodingTreeBlockLog2 - 2)
{
throw new InvalidImageContentException("The HEVC picture parameter set has an invalid parallel merge level.");
}
this.ParallelMergeLevelLog2 = (int)parallelMergeLevelMinusTwo + 2;
this.SliceSegmentHeaderExtensionPresent = reader.ReadFlag();
this.MaxTransformSkipBlockLog2 = 2;
if (reader.ReadFlag())
{
Span<bool> extensionFlags = stackalloc bool[8];
for (int extensionFlag = 0; extensionFlag < extensionFlags.Length; extensionFlag++)
{
extensionFlags[extensionFlag] = reader.ReadFlag();
}
if (extensionFlags[1])
{
throw new InvalidImageContentException("Layered HEVC picture extensions are not supported for still-image items.");
}
if (extensionFlags[0])
{
this.ReadRangeExtension(ref reader);
}
bool unknownExtensionPresent = false;
for (int extensionFlag = 2; extensionFlag < extensionFlags.Length; extensionFlag++)
{
unknownExtensionPresent |= extensionFlags[extensionFlag];
}
if (unknownExtensionPresent)
{
while (reader.HasMoreRbspData())
{
reader.ReadFlag();
}
}
}
reader.ReadRbspTrailingBits();
}
/// <summary>
/// Gets the picture-parameter-set identifier.
/// </summary>
public byte Id { get; }
/// <summary>
/// Gets the referenced sequence-parameter-set identifier.
/// </summary>
public byte SequenceParameterSetId { get; }
/// <summary>
/// Gets the sequence parameters governing this picture parameter set.
/// </summary>
public HevcSequenceParameterSet SequenceParameterSet { get; }
/// <summary>
/// Gets a value indicating whether dependent slice segments can occur.
/// </summary>
public bool DependentSliceSegmentsEnabled { get; }
/// <summary>
/// Gets a value indicating whether slice headers contain the picture-output flag.
/// </summary>
public bool OutputFlagPresent { get; }
/// <summary>
/// Gets the number of reserved extra bits at the start of each independent slice header.
/// </summary>
public int ExtraSliceHeaderBitCount { get; }
/// <summary>
/// Gets a value indicating whether transform-coefficient sign hiding is enabled.
/// </summary>
public bool SignDataHidingEnabled { get; }
/// <summary>
/// Gets a value indicating whether slices can select an alternate CABAC initialization table.
/// </summary>
public bool CabacInitializationPresent { get; }
/// <summary>
/// Gets the default active reference-index count for reference list zero.
/// </summary>
public int DefaultReferenceIndexCountList0 { get; }
/// <summary>
/// Gets the default active reference-index count for reference list one.
/// </summary>
public int DefaultReferenceIndexCountList1 { get; }
/// <summary>
/// Gets the picture quantization-parameter initializer relative to 26.
/// </summary>
public int InitialQuantizationParameterMinus26 { get; }
/// <summary>
/// Gets a value indicating whether inter-coded neighbors are excluded from intra prediction.
/// </summary>
public bool ConstrainedIntraPredictionEnabled { get; }
/// <summary>
/// Gets a value indicating whether residual transform skipping can be selected.
/// </summary>
public bool TransformSkipEnabled { get; }
/// <summary>
/// Gets a value indicating whether coding units can change the quantization parameter.
/// </summary>
public bool CodingUnitQuantizationParameterDeltaEnabled { get; }
/// <summary>
/// Gets the coding-tree depth at which quantization-parameter deltas are signaled.
/// </summary>
public int QuantizationParameterDeltaDepth { get; }
/// <summary>
/// Gets the picture-level Cb quantization-parameter offset.
/// </summary>
public int ChromaCbQuantizationParameterOffset { get; }
/// <summary>
/// Gets the picture-level Cr quantization-parameter offset.
/// </summary>
public int ChromaCrQuantizationParameterOffset { get; }
/// <summary>
/// Gets a value indicating whether slices can add Cb and Cr quantization-parameter offsets.
/// </summary>
public bool SliceChromaQuantizationParameterOffsetsPresent { get; }
/// <summary>
/// Gets a value indicating whether weighted prediction can be used by predictive slices.
/// </summary>
public bool WeightedPredictionEnabled { get; }
/// <summary>
/// Gets a value indicating whether weighted prediction can be used by bidirectional slices.
/// </summary>
public bool WeightedBiPredictionEnabled { get; }
/// <summary>
/// Gets a value indicating whether coding units can bypass transform and quantization.
/// </summary>
public bool TransquantizationBypassEnabled { get; }
/// <summary>
/// Gets a value indicating whether the coded picture is partitioned into tiles.
/// </summary>
public bool TilesEnabled { get; }
/// <summary>
/// Gets a value indicating whether wavefront entropy-coding synchronization is enabled.
/// </summary>
public bool EntropyCodingSynchronizationEnabled { get; }
/// <summary>
/// Gets a value indicating whether the tile grid uses uniform proportional spacing.
/// </summary>
public bool UniformTileSpacing { get; }
/// <summary>
/// Gets the tile-column widths in coding-tree blocks.
/// </summary>
public IReadOnlyList<int> TileColumnWidths { get; }
/// <summary>
/// Gets the tile-row heights in coding-tree blocks.
/// </summary>
public IReadOnlyList<int> TileRowHeights { get; }
/// <summary>
/// Gets a value indicating whether in-loop filtering crosses tile boundaries.
/// </summary>
public bool LoopFilterAcrossTilesEnabled { get; }
/// <summary>
/// Gets a value indicating whether in-loop filtering crosses slice boundaries.
/// </summary>
public bool LoopFilterAcrossSlicesEnabled { get; }
/// <summary>
/// Gets a value indicating whether picture or slice syntax controls deblocking.
/// </summary>
public bool DeblockingFilterControlPresent { get; }
/// <summary>
/// Gets a value indicating whether slice headers can override picture-level deblocking.
/// </summary>
public bool DeblockingFilterOverrideEnabled { get; }
/// <summary>
/// Gets a value indicating whether deblocking is disabled by default for the picture.
/// </summary>
public bool DeblockingFilterDisabled { get; }
/// <summary>
/// Gets half the picture-level deblocking beta-threshold offset.
/// </summary>
public int DeblockingFilterBetaOffsetDiv2 { get; }
/// <summary>
/// Gets half the picture-level deblocking clipping-threshold offset.
/// </summary>
public int DeblockingFilterTcOffsetDiv2 { get; }
/// <summary>
/// Gets a value indicating whether this picture parameter set supplies scaling-list data.
/// </summary>
public bool ScalingListDataPresent { get; }
/// <summary>
/// Gets the effective quantization scaling matrices for slices using this picture parameter set.
/// </summary>
public HevcScalingList ScalingList { get; }
/// <summary>
/// Gets a value indicating whether slice headers can modify the initial reference-picture lists.
/// </summary>
public bool ReferenceListModificationPresent { get; }
/// <summary>
/// Gets the base-two logarithm of the parallel merge-estimation region width and height.
/// </summary>
public int ParallelMergeLevelLog2 { get; }
/// <summary>
/// Gets a value indicating whether slice-segment headers carry extension bytes.
/// </summary>
public bool SliceSegmentHeaderExtensionPresent { get; }
/// <summary>
/// Gets the base-two logarithm of the maximum transform-skip block width and height.
/// </summary>
public int MaxTransformSkipBlockLog2 { get; private set; }
/// <summary>
/// Gets a value indicating whether cross-component residual prediction is enabled.
/// </summary>
public bool CrossComponentPredictionEnabled { get; private set; }
/// <summary>
/// Gets the coding-tree depth at which chroma quantization-offset indices are signaled.
/// </summary>
public int ChromaQuantizationParameterOffsetDepth { get; private set; }
/// <summary>
/// Gets the Cb offsets in the selectable chroma quantization-parameter offset list.
/// </summary>
public IReadOnlyList<int> ChromaQuantizationParameterOffsetsCb { get; private set; } = Array.Empty<int>();
/// <summary>
/// Gets the Cr offsets in the selectable chroma quantization-parameter offset list.
/// </summary>
public IReadOnlyList<int> ChromaQuantizationParameterOffsetsCr { get; private set; } = Array.Empty<int>();
/// <summary>
/// Gets the base-two logarithm of the luma sample-adaptive-offset value scale.
/// </summary>
public int SampleAdaptiveOffsetScaleLumaLog2 { get; private set; }
/// <summary>
/// Gets the base-two logarithm of the chroma sample-adaptive-offset value scale.
/// </summary>
public int SampleAdaptiveOffsetScaleChromaLog2 { get; private set; }
/// <summary>
/// Reads the Range Extensions fields that change transform, chroma quantization, and SAO reconstruction.
/// </summary>
/// <param name="reader">The picture-parameter-set raw byte sequence payload reader.</param>
/// <exception cref="InvalidImageContentException">
/// A transform, coding-tree-depth, chroma offset, or sample-adaptive-offset scale is outside the governing
/// sequence-parameter-set bounds.
/// </exception>
private void ReadRangeExtension(ref HevcBitReader reader)
{
if (this.TransformSkipEnabled)
{
uint maxTransformSkipBlockLog2MinusTwo = reader.ReadUnsignedExpGolomb();
if (maxTransformSkipBlockLog2MinusTwo > this.SequenceParameterSet.MaxTransformBlockLog2 - 2)
{
throw new InvalidImageContentException("The HEVC picture parameter set has an invalid transform-skip block size.");
}
this.MaxTransformSkipBlockLog2 = (int)maxTransformSkipBlockLog2MinusTwo + 2;
}
this.CrossComponentPredictionEnabled = reader.ReadFlag();
if (reader.ReadFlag())
{
uint chromaOffsetDepth = reader.ReadUnsignedExpGolomb();
int maximumDepth = this.SequenceParameterSet.CodingTreeBlockLog2 - this.SequenceParameterSet.MinCodingBlockLog2;
if (chromaOffsetDepth > maximumDepth)
{
throw new InvalidImageContentException("The HEVC picture parameter set has an invalid chroma quantization-offset depth.");
}
this.ChromaQuantizationParameterOffsetDepth = (int)chromaOffsetDepth;
uint chromaOffsetCountMinusOne = reader.ReadUnsignedExpGolomb();
if (chromaOffsetCountMinusOne > 5)
{
throw new InvalidImageContentException("The HEVC picture parameter set declares too many chroma quantization offsets.");
}
int chromaOffsetCount = (int)chromaOffsetCountMinusOne + 1;
int[] cbOffsets = new int[chromaOffsetCount];
int[] crOffsets = new int[chromaOffsetCount];
for (int offset = 0; offset < chromaOffsetCount; offset++)
{
cbOffsets[offset] = HevcParameterSetSyntax.ReadQuantizationParameterOffset(ref reader);
crOffsets[offset] = HevcParameterSetSyntax.ReadQuantizationParameterOffset(ref reader);
}
this.ChromaQuantizationParameterOffsetsCb = cbOffsets;
this.ChromaQuantizationParameterOffsetsCr = crOffsets;
}
uint lumaScale = reader.ReadUnsignedExpGolomb();
uint chromaScale = reader.ReadUnsignedExpGolomb();
int maximumLumaScale = Math.Max(this.SequenceParameterSet.BitDepthLuma, 10) - 10;
int maximumChromaScale = Math.Max(this.SequenceParameterSet.BitDepthChroma, 10) - 10;
if (lumaScale > maximumLumaScale || chromaScale > maximumChromaScale)
{
throw new InvalidImageContentException("The HEVC picture parameter set has an invalid sample-adaptive-offset scale.");
}
this.SampleAdaptiveOffsetScaleLumaLog2 = (int)lumaScale;
this.SampleAdaptiveOffsetScaleChromaLog2 = (int)chromaScale;
}
/// <summary>
/// Reads or derives one axis of the tile grid.
/// </summary>
/// <param name="reader">The picture-parameter-set raw byte sequence payload reader.</param>
/// <param name="codingTreeBlockCount">The complete picture dimension in coding-tree blocks.</param>
/// <param name="tileCount">The tile count on the same axis.</param>
/// <param name="uniformSpacing">Whether the widths or heights use proportional uniform spacing.</param>
/// <returns>Every tile width or height in coding-tree blocks, including the inferred final tile.</returns>
/// <exception cref="InvalidImageContentException">An explicit tile consumes the final block required by a later tile.</exception>
private static int[] ReadTileDimensions(
ref HevcBitReader reader,
int codingTreeBlockCount,
int tileCount,
bool uniformSpacing)
{
int[] dimensions = new int[tileCount];
if (uniformSpacing)
{
for (int tile = 0; tile < tileCount; tile++)
{
// The normative floor-difference formula assigns every CTB exactly once even when the picture
// dimension is not divisible by the number of tiles.
dimensions[tile] = (((tile + 1) * codingTreeBlockCount) / tileCount)
- ((tile * codingTreeBlockCount) / tileCount);
}
return dimensions;
}
int consumed = 0;
for (int tile = 0; tile < tileCount - 1; tile++)
{
uint dimensionMinusOne = reader.ReadUnsignedExpGolomb();
if (dimensionMinusOne >= codingTreeBlockCount - consumed - 1)
{
throw new InvalidImageContentException("The HEVC picture parameter set has an invalid explicit tile dimension.");
}
dimensions[tile] = (int)dimensionMinusOne + 1;
consumed += dimensions[tile];
}
dimensions[^1] = codingTreeBlockCount - consumed;
return dimensions;
}
}

25
src/ImageSharp/Formats/Heif/Hevc/HevcPlane.cs

@ -1,25 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Identifies an HEVC luma or chroma reconstruction plane.
/// </summary>
internal enum HevcPlane
{
/// <summary>
/// The luma or first separate-color plane.
/// </summary>
Y = 0,
/// <summary>
/// The blue-difference chroma or second separate-color plane.
/// </summary>
Cb = 1,
/// <summary>
/// The red-difference chroma or third separate-color plane.
/// </summary>
Cr = 2,
}

115
src/ImageSharp/Formats/Heif/Hevc/HevcProfileTierLevel.cs

@ -1,115 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Contains the general HEVC profile, tier, constraint, and level description declared by a parameter set.
/// </summary>
internal sealed class HevcProfileTierLevel
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcProfileTierLevel"/> class.
/// </summary>
/// <param name="reader">The parameter-set raw byte sequence payload reader.</param>
/// <param name="maxSubLayersMinusOne">The highest declared temporal sublayer index.</param>
/// <exception cref="InvalidImageContentException">
/// The profile-tier-level syntax is truncated or contains nonzero reserved bits.
/// </exception>
public HevcProfileTierLevel(ref HevcBitReader reader, int maxSubLayersMinusOne)
{
DebugGuard.MustBeBetweenOrEqualTo(maxSubLayersMinusOne, 0, 6, nameof(maxSubLayersMinusOne));
this.ProfileSpace = (byte)reader.ReadBits(2);
this.TierFlag = reader.ReadFlag();
this.ProfileIdc = (byte)reader.ReadBits(5);
this.ProfileCompatibilityFlags = reader.ReadBits(32);
// The configuration record carries these 48 bits verbatim. Preserve their exact ordering so the
// parameter set can be checked without reinterpreting profile-specific constraint layouts.
this.ConstraintIndicatorFlags = ((ulong)reader.ReadBits(16) << 32) | reader.ReadBits(32);
this.LevelIdc = (byte)reader.ReadBits(8);
Span<bool> subLayerProfilePresent = stackalloc bool[6];
Span<bool> subLayerLevelPresent = stackalloc bool[6];
for (int subLayer = 0; subLayer < maxSubLayersMinusOne; subLayer++)
{
subLayerProfilePresent[subLayer] = reader.ReadFlag();
subLayerLevelPresent[subLayer] = reader.ReadFlag();
}
if (maxSubLayersMinusOne > 0)
{
for (int subLayer = maxSubLayersMinusOne; subLayer < 8; subLayer++)
{
if (reader.ReadBits(2) != 0)
{
throw new InvalidImageContentException("The HEVC profile-tier-level syntax has nonzero reserved bits.");
}
}
}
for (int subLayer = 0; subLayer < maxSubLayersMinusOne; subLayer++)
{
if (subLayerProfilePresent[subLayer])
{
// A sublayer profile repeats the fixed 88-bit profile and constraint structure. It is consumed
// for alignment but not retained because one still-image item has no temporal playback model.
reader.ReadBits(2);
reader.ReadFlag();
reader.ReadBits(5);
reader.ReadBits(32);
reader.ReadBits(16);
reader.ReadBits(32);
}
if (subLayerLevelPresent[subLayer])
{
reader.ReadBits(8);
}
}
}
/// <summary>
/// Gets the namespace of the declared profile identifier.
/// </summary>
public byte ProfileSpace { get; }
/// <summary>
/// Gets a value indicating whether the high tier is declared.
/// </summary>
public bool TierFlag { get; }
/// <summary>
/// Gets the five-bit profile identifier.
/// </summary>
public byte ProfileIdc { get; }
/// <summary>
/// Gets the profile-compatibility flags.
/// </summary>
public uint ProfileCompatibilityFlags { get; }
/// <summary>
/// Gets the 48-bit profile-constraint flags.
/// </summary>
public ulong ConstraintIndicatorFlags { get; }
/// <summary>
/// Gets the eight-bit level identifier.
/// </summary>
public byte LevelIdc { get; }
/// <summary>
/// Determines whether this parameter-set description is compatible with an image item's codec-configuration
/// property.
/// </summary>
/// <param name="configuration">The associated HEVC codec configuration.</param>
/// <returns><see langword="true"/> when the general profile, tier, compatibility, and level fields match.</returns>
public bool Matches(HevcCodecConfiguration configuration)
=> this.ProfileSpace == configuration.GeneralProfileSpace
&& this.TierFlag == configuration.GeneralTierFlag
&& this.ProfileIdc == configuration.GeneralProfileIdc
&& this.ProfileCompatibilityFlags == configuration.GeneralProfileCompatibilityFlags
&& this.LevelIdc == configuration.GeneralLevelIdc;
}

111
src/ImageSharp/Formats/Heif/Hevc/HevcQuantizationParameters.cs

@ -1,111 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Contains the effective HEVC quantization parameters for one transform unit.
/// </summary>
internal readonly struct HevcQuantizationParameters
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcQuantizationParameters"/> struct.
/// </summary>
/// <param name="lumaQuantizationParameter">The effective coding-unit luma quantization parameter before the luma bit-depth offset.</param>
/// <param name="lumaBitDepth">The reconstructed luma precision.</param>
/// <param name="chromaBitDepth">The reconstructed chroma precision.</param>
/// <param name="chromaFormat">The sequence chroma-format identifier.</param>
/// <param name="cbQuantizationParameterOffset">The combined picture, slice, and coding-unit Cb quantization-parameter offset.</param>
/// <param name="crQuantizationParameterOffset">The combined picture, slice, and coding-unit Cr quantization-parameter offset.</param>
public HevcQuantizationParameters(
int lumaQuantizationParameter,
int lumaBitDepth,
int chromaBitDepth,
byte chromaFormat,
int cbQuantizationParameterOffset,
int crQuantizationParameterOffset)
{
int lumaBitDepthOffset = 6 * (lumaBitDepth - 8);
int chromaBitDepthOffset = 6 * (chromaBitDepth - 8);
this.CbOffset = cbQuantizationParameterOffset;
this.CrOffset = crQuantizationParameterOffset;
this.Luma = lumaQuantizationParameter + lumaBitDepthOffset;
this.Cb = GetChromaQuantizationParameter(lumaQuantizationParameter, cbQuantizationParameterOffset, chromaBitDepthOffset, chromaFormat);
this.Cr = GetChromaQuantizationParameter(lumaQuantizationParameter, crQuantizationParameterOffset, chromaBitDepthOffset, chromaFormat);
}
/// <summary>
/// Gets the effective nonnegative luma quantization parameter including its bit-depth offset.
/// </summary>
public int Luma { get; }
/// <summary>
/// Gets the effective nonnegative blue-difference chroma quantization parameter including its bit-depth offset.
/// </summary>
public int Cb { get; }
/// <summary>
/// Gets the effective nonnegative red-difference chroma quantization parameter including its bit-depth offset.
/// </summary>
public int Cr { get; }
/// <summary>
/// Gets the combined picture, slice, and coding-unit Cb quantization-parameter offset.
/// </summary>
public int CbOffset { get; }
/// <summary>
/// Gets the combined picture, slice, and coding-unit Cr quantization-parameter offset.
/// </summary>
public int CrOffset { get; }
/// <summary>
/// Gets the H.265 Table 8-10 chroma quantization-parameter mapping for 4:2:0 pictures.
/// </summary>
private static ReadOnlySpan<byte> Chroma420QuantizationParameterMap =>
[
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
29, 30, 31, 32, 33, 33, 34, 34, 35, 35, 36, 36, 37, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,
];
/// <summary>
/// Gets the effective quantization parameter for the selected reconstruction plane.
/// </summary>
/// <param name="plane">The reconstruction plane.</param>
/// <returns>The effective nonnegative quantization parameter including its bit-depth offset.</returns>
public int Get(HevcPlane plane) => plane switch
{
HevcPlane.Y => this.Luma,
HevcPlane.Cb => this.Cb,
_ => this.Cr,
};
/// <summary>
/// Derives an effective chroma quantization parameter from the luma value and combined component offset.
/// </summary>
/// <param name="lumaQuantizationParameter">The effective coding-unit luma quantization parameter before its bit-depth offset.</param>
/// <param name="componentOffset">The combined picture, slice, and coding-unit component offset.</param>
/// <param name="chromaBitDepthOffset">Six times the number of chroma bits above eight.</param>
/// <param name="chromaFormat">The sequence chroma-format identifier.</param>
/// <returns>The effective nonnegative chroma quantization parameter including its bit-depth offset.</returns>
public static int GetChromaQuantizationParameter(
int lumaQuantizationParameter,
int componentOffset,
int chromaBitDepthOffset,
byte chromaFormat)
{
int unscaled = Math.Clamp(lumaQuantizationParameter + componentOffset, -chromaBitDepthOffset, 57);
if (unscaled < 0)
{
return unscaled + chromaBitDepthOffset;
}
// H.265 section 8.6.1 maps nonnegative chroma QP before adding the bit-depth offset. The 4:2:0 table
// contains plateaus above QP 29, whereas 4:2:2 and 4:4:4 remain linear through 51 and then saturate.
int mapped = chromaFormat == 1
? Chroma420QuantizationParameterMap[unscaled]
: Math.Min(unscaled, 51);
return mapped + chromaBitDepthOffset;
}
}

235
src/ImageSharp/Formats/Heif/Hevc/HevcReconstructionState.cs

@ -1,235 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Tracks reconstructed minimum prediction blocks for HEVC intra-reference availability.
/// </summary>
internal sealed class HevcReconstructionState : IDisposable
{
/// <summary>
/// The base-two logarithm of the minimum luma prediction-block side.
/// </summary>
private const int MinPredictionBlockLog2 = 2;
/// <summary>
/// The reconstruction-region identifiers for the three component planes.
/// </summary>
private readonly Buffer2D<int>[] regions;
/// <summary>
/// The horizontal chroma subsampling shift.
/// </summary>
private readonly int chromaSubsamplingX;
/// <summary>
/// The vertical chroma subsampling shift.
/// </summary>
private readonly int chromaSubsamplingY;
/// <summary>
/// The coded luma width used to reject padded right-edge units.
/// </summary>
private readonly int width;
/// <summary>
/// The coded luma height used to reject padded bottom-edge units.
/// </summary>
private readonly int height;
/// <summary>
/// Initializes a new instance of the <see cref="HevcReconstructionState"/> class.
/// </summary>
/// <param name="configuration">The configuration providing the image memory allocator.</param>
/// <param name="sequenceParameterSet">The coded picture and chroma geometry.</param>
public HevcReconstructionState(Configuration configuration, HevcSequenceParameterSet sequenceParameterSet)
{
this.width = sequenceParameterSet.Width;
this.height = sequenceParameterSet.Height;
int widthInUnits = DivideCeilingByPowerOfTwo(this.width, MinPredictionBlockLog2);
int heightInUnits = DivideCeilingByPowerOfTwo(this.height, MinPredictionBlockLog2);
this.chromaSubsamplingX = !sequenceParameterSet.SeparateColorPlaneFlag && sequenceParameterSet.ChromaFormat is 1 or 2 ? 1 : 0;
this.chromaSubsamplingY = !sequenceParameterSet.SeparateColorPlaneFlag && sequenceParameterSet.ChromaFormat == 1 ? 1 : 0;
Buffer2D<int>? lumaRegions = null;
Buffer2D<int>? chromaBlueRegions = null;
Buffer2D<int>? chromaRedRegions = null;
try
{
// Region identifiers gate every reconstructed-neighbor read. A stale pooled identifier can match the first
// region of a later picture, so these maps must begin at the reserved unavailable value zero.
lumaRegions = configuration.MemoryAllocator.Allocate2D<int>(widthInUnits, heightInUnits, AllocationOptions.Clean);
chromaBlueRegions = configuration.MemoryAllocator.Allocate2D<int>(widthInUnits, heightInUnits, AllocationOptions.Clean);
chromaRedRegions = configuration.MemoryAllocator.Allocate2D<int>(widthInUnits, heightInUnits, AllocationOptions.Clean);
this.regions = [lumaRegions, chromaBlueRegions, chromaRedRegions];
}
catch
{
chromaRedRegions?.Dispose();
chromaBlueRegions?.Dispose();
lumaRegions?.Dispose();
throw;
}
}
/// <summary>
/// Gets the horizontal availability-unit width for a component plane.
/// </summary>
/// <param name="plane">The component plane.</param>
/// <returns>The availability-unit width in component samples.</returns>
public int GetUnitWidth(HevcPlane plane) => 1 << (MinPredictionBlockLog2 - this.GetSubsamplingX(plane));
/// <summary>
/// Gets the vertical availability-unit height for a component plane.
/// </summary>
/// <param name="plane">The component plane.</param>
/// <returns>The availability-unit height in component samples.</returns>
public int GetUnitHeight(HevcPlane plane) => 1 << (MinPredictionBlockLog2 - this.GetSubsamplingY(plane));
/// <summary>
/// Marks a reconstructed component rectangle as available within one slice-and-tile prediction region.
/// </summary>
/// <param name="plane">The reconstructed component plane.</param>
/// <param name="x">The rectangle left coordinate in component samples.</param>
/// <param name="y">The rectangle top coordinate in component samples.</param>
/// <param name="width">The rectangle width in component samples.</param>
/// <param name="height">The rectangle height in component samples.</param>
/// <param name="regionId">The positive identifier shared by prediction blocks in the same slice segment and tile.</param>
public void MarkReconstructed(HevcPlane plane, int x, int y, int width, int height, int regionId)
{
DebugGuard.MustBeGreaterThan(regionId, 0, nameof(regionId));
int subsamplingX = this.GetSubsamplingX(plane);
int subsamplingY = this.GetSubsamplingY(plane);
int unitX = (x << subsamplingX) >> MinPredictionBlockLog2;
int unitY = (y << subsamplingY) >> MinPredictionBlockLog2;
int endX = DivideCeilingByPowerOfTwo((x + width) << subsamplingX, MinPredictionBlockLog2);
int endY = DivideCeilingByPowerOfTwo((y + height) << subsamplingY, MinPredictionBlockLog2);
Buffer2D<int> map = this.regions[(int)plane];
endX = Math.Min(endX, map.Width);
endY = Math.Min(endY, map.Height);
// Chroma availability units map back to the same four-by-four luma grid used by HEVC neighbor derivation.
// Filling the complete rectangle makes later sub-TUs observe only samples whose reconstruction has finished.
for (int row = unitY; row < endY; row++)
{
map.DangerousGetRowSpan(row)[unitX..endX].Fill(regionId);
}
}
/// <summary>
/// Builds the ordered availability flags consumed by HEVC reference-sample substitution.
/// </summary>
/// <param name="plane">The component plane containing the prediction block.</param>
/// <param name="x">The prediction-block left coordinate in component samples.</param>
/// <param name="y">The prediction-block top coordinate in component samples.</param>
/// <param name="log2Size">The base-two logarithm of the square prediction-block side.</param>
/// <param name="regionId">The current slice-and-tile prediction-region identifier.</param>
/// <param name="destination">
/// The destination ordered from the bottom-most below-left unit through top-left and then the above-right units.
/// </param>
/// <returns>The number of flags written.</returns>
public int BuildReferenceAvailability(HevcPlane plane, int x, int y, int log2Size, int regionId, Span<bool> destination)
{
DebugGuard.MustBeBetweenOrEqualTo(log2Size, 2, 5, nameof(log2Size));
DebugGuard.MustBeGreaterThan(regionId, 0, nameof(regionId));
int size = 1 << log2Size;
int unitWidth = this.GetUnitWidth(plane);
int unitHeight = this.GetUnitHeight(plane);
int leftUnitCount = (size * 2) / unitHeight;
int aboveUnitCount = (size * 2) / unitWidth;
int flagCount = leftUnitCount + aboveUnitCount + 1;
Span<bool> availability = destination[..flagCount];
for (int unit = 0; unit < leftUnitCount; unit++)
{
int unitY = y + ((leftUnitCount - unit - 1) * unitHeight);
availability[unit] = this.IsAvailable(plane, x - 1, unitY, regionId);
}
availability[leftUnitCount] = this.IsAvailable(plane, x - 1, y - 1, regionId);
for (int unit = 0; unit < aboveUnitCount; unit++)
{
availability[leftUnitCount + unit + 1] = this.IsAvailable(plane, x + (unit * unitWidth), y - 1, regionId);
}
return flagCount;
}
/// <summary>
/// Gets whether one component sample has already been reconstructed in the selected prediction region.
/// </summary>
/// <param name="plane">The component plane.</param>
/// <param name="x">The component sample X coordinate.</param>
/// <param name="y">The component sample Y coordinate.</param>
/// <param name="regionId">The current slice-and-tile prediction-region identifier.</param>
/// <returns><see langword="true"/> when the sample is available; otherwise, <see langword="false"/>.</returns>
public bool IsReconstructed(HevcPlane plane, int x, int y, int regionId) => this.IsAvailable(plane, x, y, regionId);
/// <summary>
/// Releases the owned reconstruction-region maps.
/// </summary>
public void Dispose()
{
foreach (Buffer2D<int> map in this.regions)
{
map.Dispose();
}
}
/// <summary>
/// Gets whether a component sample belongs to an already reconstructed block in the selected prediction region.
/// </summary>
/// <param name="plane">The component plane.</param>
/// <param name="x">The component sample X coordinate.</param>
/// <param name="y">The component sample Y coordinate.</param>
/// <param name="regionId">The current slice-and-tile prediction-region identifier.</param>
/// <returns><see langword="true"/> when the sample is available; otherwise, <see langword="false"/>.</returns>
private bool IsAvailable(HevcPlane plane, int x, int y, int regionId)
{
if (x < 0 || y < 0)
{
return false;
}
int subsamplingX = this.GetSubsamplingX(plane);
int subsamplingY = this.GetSubsamplingY(plane);
int planeWidth = DivideCeilingByPowerOfTwo(this.width, subsamplingX);
int planeHeight = DivideCeilingByPowerOfTwo(this.height, subsamplingY);
if (x >= planeWidth || y >= planeHeight)
{
return false;
}
int unitX = (x << subsamplingX) >> MinPredictionBlockLog2;
int unitY = (y << subsamplingY) >> MinPredictionBlockLog2;
Buffer2D<int> map = this.regions[(int)plane];
return (uint)unitX < (uint)map.Width
&& (uint)unitY < (uint)map.Height
&& map.DangerousGetRowSpan(unitY)[unitX] == regionId;
}
/// <summary>
/// Gets the horizontal chroma shift selected by a component plane.
/// </summary>
/// <param name="plane">The component plane.</param>
/// <returns>Zero for luma and full-resolution planes; otherwise, the chroma shift.</returns>
private int GetSubsamplingX(HevcPlane plane) => plane == HevcPlane.Y ? 0 : this.chromaSubsamplingX;
/// <summary>
/// Gets the vertical chroma shift selected by a component plane.
/// </summary>
/// <param name="plane">The component plane.</param>
/// <returns>Zero for luma and full-resolution planes; otherwise, the chroma shift.</returns>
private int GetSubsamplingY(HevcPlane plane) => plane == HevcPlane.Y ? 0 : this.chromaSubsamplingY;
/// <summary>
/// Divides a nonnegative sample count by a power of two with upward rounding.
/// </summary>
/// <param name="value">The sample count.</param>
/// <param name="shift">The base-two divisor logarithm.</param>
/// <returns>The upward-rounded quotient.</returns>
private static int DivideCeilingByPowerOfTwo(int value, int shift) => (value + (1 << shift) - 1) >> shift;
}

25
src/ImageSharp/Formats/Heif/Hevc/HevcResidualDpcmMode.cs

@ -1,25 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Identifies the differential pulse-code modulation applied to an HEVC residual block.
/// </summary>
internal enum HevcResidualDpcmMode : byte
{
/// <summary>
/// No residual differential pulse-code modulation is applied.
/// </summary>
None = 0,
/// <summary>
/// Residual differences accumulate from left to right within each row.
/// </summary>
Horizontal = 1,
/// <summary>
/// Residual differences accumulate from top to bottom within each column.
/// </summary>
Vertical = 2,
}

32
src/ImageSharp/Formats/Heif/Hevc/HevcResidualReconstructor.LeftShiftTransformSkipOperator.cs

@ -1,32 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
internal static partial class HevcResidualReconstructor
{
/// <summary>
/// Applies the exact left shift used by high-bit-depth transform-skip reconstruction.
/// </summary>
private readonly struct LeftShiftTransformSkipOperator : ITransformSkipOperator
{
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<int> Invoke(Vector512<int> values, int shift) => values << shift;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<int> Invoke(Vector256<int> values, int shift) => values << shift;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<int> Invoke(Vector128<int> values, int shift) => values << shift;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Invoke(int value, int shift) => value << shift;
}
}

47
src/ImageSharp/Formats/Heif/Hevc/HevcResidualReconstructor.Operator.cs

@ -1,47 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
internal static partial class HevcResidualReconstructor
{
/// <summary>
/// Defines a closed transform-skip normalization operator for every SIMD width and the scalar tail.
/// </summary>
private interface ITransformSkipOperator
{
/// <summary>
/// Normalizes sixteen transform-skipped coefficients.
/// </summary>
/// <param name="values">The dequantized coefficients.</param>
/// <param name="shift">The nonnegative shift magnitude.</param>
/// <returns>The reconstructed residuals.</returns>
static abstract Vector512<int> Invoke(Vector512<int> values, int shift);
/// <summary>
/// Normalizes eight transform-skipped coefficients.
/// </summary>
/// <param name="values">The dequantized coefficients.</param>
/// <param name="shift">The nonnegative shift magnitude.</param>
/// <returns>The reconstructed residuals.</returns>
static abstract Vector256<int> Invoke(Vector256<int> values, int shift);
/// <summary>
/// Normalizes four transform-skipped coefficients.
/// </summary>
/// <param name="values">The dequantized coefficients.</param>
/// <param name="shift">The nonnegative shift magnitude.</param>
/// <returns>The reconstructed residuals.</returns>
static abstract Vector128<int> Invoke(Vector128<int> values, int shift);
/// <summary>
/// Normalizes one transform-skipped coefficient.
/// </summary>
/// <param name="value">The dequantized coefficient.</param>
/// <param name="shift">The nonnegative shift magnitude.</param>
/// <returns>The reconstructed residual.</returns>
static abstract int Invoke(int value, int shift);
}
}

35
src/ImageSharp/Formats/Heif/Hevc/HevcResidualReconstructor.RightShiftTransformSkipOperator.cs

@ -1,35 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
internal static partial class HevcResidualReconstructor
{
/// <summary>
/// Applies the rounded right shift used by ordinary transform-skip reconstruction.
/// </summary>
private readonly struct RightShiftTransformSkipOperator : ITransformSkipOperator
{
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<int> Invoke(Vector512<int> values, int shift)
=> shift == 0 ? values : (values + Vector512.Create(1 << (shift - 1))) >> shift;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<int> Invoke(Vector256<int> values, int shift)
=> shift == 0 ? values : (values + Vector256.Create(1 << (shift - 1))) >> shift;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<int> Invoke(Vector128<int> values, int shift)
=> shift == 0 ? values : (values + Vector128.Create(1 << (shift - 1))) >> shift;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Invoke(int value, int shift) => shift == 0 ? value : (value + (1 << (shift - 1))) >> shift;
}
}

578
src/ImageSharp/Formats/Heif/Hevc/HevcResidualReconstructor.cs

@ -1,578 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Reconstructs HEVC transform-skipped, bypassed, and differential residual blocks.
/// </summary>
/// <remarks>
/// Consecutive residual samples are widened to signed 32-bit lanes for normalization and prediction addition. Closed
/// static operators encode the selected transform-skip shift so the JIT specializes left-shift, rounded-right-shift,
/// and identity cases outside the row loops. Saturation to the residual range and clipping to sample depth occur at the
/// same stage in every vector width and in the scalar tail.
/// </remarks>
internal static partial class HevcResidualReconstructor
{
/// <summary>
/// The minimum residual sample represented by the decoder reconstruction pipeline.
/// </summary>
private const int ResidualMinimum = short.MinValue;
/// <summary>
/// The maximum residual sample represented by the decoder reconstruction pipeline.
/// </summary>
private const int ResidualMaximum = short.MaxValue;
/// <summary>
/// Copies one transquant-bypass coefficient block into residual sample order.
/// </summary>
/// <param name="coefficients">The decoded coefficients in raster order.</param>
/// <param name="residual">The destination residual block in packed raster order.</param>
/// <param name="rotate">Whether the complete coefficient order is reversed.</param>
public static void CopyBypassed(ReadOnlySpan<int> coefficients, Span<int> residual, bool rotate)
{
Span<int> destination = residual[..coefficients.Length];
if (!rotate)
{
coefficients.CopyTo(destination);
return;
}
CopyReversed(coefficients, destination);
}
/// <summary>
/// Reconstructs one transform-skipped residual block from dequantized coefficients.
/// </summary>
/// <param name="coefficients">The dequantized coefficients in raster order.</param>
/// <param name="residual">The destination residual block in packed raster order.</param>
/// <param name="width">The transform-block width.</param>
/// <param name="height">The transform-block height.</param>
/// <param name="bitDepth">The reconstructed component precision.</param>
/// <param name="maxTransformDynamicRange">The transform dynamic range excluding its sign bit.</param>
/// <param name="equivalentLog2TransformSize">The base-two logarithm of the equivalent square transform size.</param>
/// <param name="extendedPrecisionProcessingEnabled">Whether transform-skip precision is extended by the sequence.</param>
/// <param name="rotate">Whether the complete coefficient order is reversed.</param>
public static void ApplyTransformSkip(
ReadOnlySpan<int> coefficients,
Span<int> residual,
int width,
int height,
int bitDepth,
int maxTransformDynamicRange,
int equivalentLog2TransformSize,
bool extendedPrecisionProcessingEnabled,
bool rotate)
{
int transformShift = maxTransformDynamicRange - bitDepth - equivalentLog2TransformSize;
if (extendedPrecisionProcessingEnabled)
{
transformShift = Math.Max(0, transformShift);
}
int coefficientCount = width * height;
if (transformShift >= 0)
{
ApplyTransformSkip<RightShiftTransformSkipOperator>(coefficients[..coefficientCount], residual[..coefficientCount], transformShift, rotate);
}
else
{
ApplyTransformSkip<LeftShiftTransformSkipOperator>(coefficients[..coefficientCount], residual[..coefficientCount], -transformShift, rotate);
}
}
/// <summary>
/// Gets whether a non-transformed residual block uses the HEVC Range Extensions coefficient rotation.
/// </summary>
/// <param name="transformSkipRotationEnabled">Whether the sequence enables transform-skip rotation.</param>
/// <param name="isIntraPredicted">Whether the transform unit belongs to an intra-predicted coding unit.</param>
/// <param name="width">The transform-block width.</param>
/// <returns><see langword="true"/> when the complete coefficient order is reversed; otherwise, <see langword="false"/>.</returns>
public static bool IsNonTransformedResidualRotated(bool transformSkipRotationEnabled, bool isIntraPredicted, int width)
=> transformSkipRotationEnabled && isIntraPredicted && width == 4;
/// <summary>
/// Gets the implicit residual differential mode selected by an intra-prediction direction.
/// </summary>
/// <param name="intraPredictionMode">The resolved luma or chroma intra-prediction mode.</param>
/// <param name="remapChroma422">Whether the 4:2:2 chroma intra-angle remapping applies.</param>
/// <returns>The residual differential mode selected by the prediction direction.</returns>
public static HevcResidualDpcmMode GetImplicitResidualDpcmMode(int intraPredictionMode, bool remapChroma422)
{
int predictionMode = remapChroma422 ? HevcIntraPredictionMode.RemapChroma422(intraPredictionMode) : intraPredictionMode;
return predictionMode switch
{
HevcIntraPredictionMode.Horizontal => HevcResidualDpcmMode.Horizontal,
HevcIntraPredictionMode.Vertical => HevcResidualDpcmMode.Vertical,
_ => HevcResidualDpcmMode.None,
};
}
/// <summary>
/// Applies inverse residual differential pulse-code modulation to one packed residual block.
/// </summary>
/// <param name="residual">The residual block in packed raster order.</param>
/// <param name="width">The residual-block width.</param>
/// <param name="height">The residual-block height.</param>
/// <param name="mode">The differential accumulation direction.</param>
public static void ApplyResidualDpcm(Span<int> residual, int width, int height, HevcResidualDpcmMode mode)
{
if (mode == HevcResidualDpcmMode.Vertical)
{
ApplyVerticalResidualDpcm(residual, width, height);
}
else if (mode == HevcResidualDpcmMode.Horizontal)
{
ApplyHorizontalResidualDpcm(residual, width, height);
}
}
/// <summary>
/// Adds the scaled luma residual to one chroma residual block for inverse cross-component prediction.
/// </summary>
/// <param name="lumaResidual">The packed luma residual samples colocated with the chroma block.</param>
/// <param name="chromaResidual">The packed chroma residual block updated in place.</param>
/// <param name="sampleCount">The number of residual samples in each block.</param>
/// <param name="alpha">The signed cross-component scale from minus eight through eight.</param>
/// <param name="bitDepthDifference">The luma bit depth minus the chroma bit depth.</param>
public static void ApplyCrossComponentPrediction(
ReadOnlySpan<int> lumaResidual,
Span<int> chromaResidual,
int sampleCount,
int alpha,
int bitDepthDifference)
{
ref int lumaBase = ref MemoryMarshal.GetReference(lumaResidual);
ref int chromaBase = ref MemoryMarshal.GetReference(chromaResidual);
int index = 0;
// The scale denominator is eight. Adjusting luma precision first preserves the normative arithmetic shift
// for negative residuals before the signed alpha multiplication is applied independently to every lane.
if (Vector512.IsHardwareAccelerated)
{
Vector512<int> alphaVector = Vector512.Create(alpha);
Vector512<int> minimum = Vector512.Create(ResidualMinimum);
Vector512<int> maximum = Vector512.Create(ResidualMaximum);
for (; index <= sampleCount - Vector512<int>.Count; index += Vector512<int>.Count)
{
Vector512<int> luma = AdjustBitDepth(Vector512.LoadUnsafe(ref lumaBase, (nuint)index), bitDepthDifference);
Vector512<int> chroma = Vector512.LoadUnsafe(ref chromaBase, (nuint)index);
Vector512.Clamp(chroma + ((luma * alphaVector) >> 3), minimum, maximum).StoreUnsafe(ref chromaBase, (nuint)index);
}
}
if (Vector256.IsHardwareAccelerated)
{
Vector256<int> alphaVector = Vector256.Create(alpha);
Vector256<int> minimum = Vector256.Create(ResidualMinimum);
Vector256<int> maximum = Vector256.Create(ResidualMaximum);
for (; index <= sampleCount - Vector256<int>.Count; index += Vector256<int>.Count)
{
Vector256<int> luma = AdjustBitDepth(Vector256.LoadUnsafe(ref lumaBase, (nuint)index), bitDepthDifference);
Vector256<int> chroma = Vector256.LoadUnsafe(ref chromaBase, (nuint)index);
Vector256.Clamp(chroma + ((luma * alphaVector) >> 3), minimum, maximum).StoreUnsafe(ref chromaBase, (nuint)index);
}
}
if (Vector128.IsHardwareAccelerated)
{
Vector128<int> alphaVector = Vector128.Create(alpha);
Vector128<int> minimum = Vector128.Create(ResidualMinimum);
Vector128<int> maximum = Vector128.Create(ResidualMaximum);
for (; index <= sampleCount - Vector128<int>.Count; index += Vector128<int>.Count)
{
Vector128<int> luma = AdjustBitDepth(Vector128.LoadUnsafe(ref lumaBase, (nuint)index), bitDepthDifference);
Vector128<int> chroma = Vector128.LoadUnsafe(ref chromaBase, (nuint)index);
Vector128.Clamp(chroma + ((luma * alphaVector) >> 3), minimum, maximum).StoreUnsafe(ref chromaBase, (nuint)index);
}
}
for (; index < sampleCount; index++)
{
int luma = AdjustBitDepth(Unsafe.Add(ref lumaBase, index), bitDepthDifference);
int chroma = Unsafe.Add(ref chromaBase, index) + ((alpha * luma) >> 3);
Unsafe.Add(ref chromaBase, index) = Math.Clamp(chroma, ResidualMinimum, ResidualMaximum);
}
}
/// <summary>
/// Adjusts sixteen luma residuals to chroma precision.
/// </summary>
/// <param name="values">The luma residuals.</param>
/// <param name="difference">The luma bit depth minus the chroma bit depth.</param>
/// <returns>The precision-adjusted residuals.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<int> AdjustBitDepth(Vector512<int> values, int difference)
=> difference >= 0 ? values >> difference : values << -difference;
/// <summary>
/// Adjusts eight luma residuals to chroma precision.
/// </summary>
/// <param name="values">The luma residuals.</param>
/// <param name="difference">The luma bit depth minus the chroma bit depth.</param>
/// <returns>The precision-adjusted residuals.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<int> AdjustBitDepth(Vector256<int> values, int difference)
=> difference >= 0 ? values >> difference : values << -difference;
/// <summary>
/// Adjusts four luma residuals to chroma precision.
/// </summary>
/// <param name="values">The luma residuals.</param>
/// <param name="difference">The luma bit depth minus the chroma bit depth.</param>
/// <returns>The precision-adjusted residuals.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<int> AdjustBitDepth(Vector128<int> values, int difference)
=> difference >= 0 ? values >> difference : values << -difference;
/// <summary>
/// Adjusts one luma residual to chroma precision.
/// </summary>
/// <param name="value">The luma residual.</param>
/// <param name="difference">The luma bit depth minus the chroma bit depth.</param>
/// <returns>The precision-adjusted residual.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int AdjustBitDepth(int value, int difference) => difference >= 0 ? value >> difference : value << -difference;
/// <summary>
/// Applies one transform-skip normalization operator to a complete coefficient block.
/// </summary>
/// <typeparam name="TOperator">The signed shift operator selected before entering the hot loop.</typeparam>
/// <param name="coefficients">The dequantized coefficients in raster order.</param>
/// <param name="residual">The destination residual block in packed raster order.</param>
/// <param name="shift">The nonnegative shift magnitude.</param>
/// <param name="rotate">Whether the complete coefficient order is reversed.</param>
private static void ApplyTransformSkip<TOperator>(ReadOnlySpan<int> coefficients, Span<int> residual, int shift, bool rotate)
where TOperator : struct, ITransformSkipOperator
{
ref int sourceBase = ref MemoryMarshal.GetReference(coefficients);
ref int destinationBase = ref MemoryMarshal.GetReference(residual);
int count = coefficients.Length;
int index = 0;
// Rotation reverses the complete raster sequence, not the lanes of independently loaded forward chunks. Each
// load therefore starts at the mirrored chunk and shuffles its lanes before the common destination traversal.
if (Vector512.IsHardwareAccelerated)
{
for (; index <= count - Vector512<int>.Count; index += Vector512<int>.Count)
{
Vector512<int> values = Load512(ref sourceBase, count, index, rotate);
TOperator.Invoke(values, shift).StoreUnsafe(ref destinationBase, (nuint)index);
}
}
if (Vector256.IsHardwareAccelerated)
{
for (; index <= count - Vector256<int>.Count; index += Vector256<int>.Count)
{
Vector256<int> values = Load256(ref sourceBase, count, index, rotate);
TOperator.Invoke(values, shift).StoreUnsafe(ref destinationBase, (nuint)index);
}
}
if (Vector128.IsHardwareAccelerated)
{
for (; index <= count - Vector128<int>.Count; index += Vector128<int>.Count)
{
Vector128<int> values = Load128(ref sourceBase, count, index, rotate);
TOperator.Invoke(values, shift).StoreUnsafe(ref destinationBase, (nuint)index);
}
}
for (; index < count; index++)
{
int sourceIndex = rotate ? count - 1 - index : index;
Unsafe.Add(ref destinationBase, index) = TOperator.Invoke(Unsafe.Add(ref sourceBase, sourceIndex), shift);
}
}
/// <summary>
/// Copies one coefficient block while reversing its complete raster order.
/// </summary>
/// <param name="source">The source coefficient block.</param>
/// <param name="destination">The destination residual block.</param>
private static void CopyReversed(ReadOnlySpan<int> source, Span<int> destination)
{
ref int sourceBase = ref MemoryMarshal.GetReference(source);
ref int destinationBase = ref MemoryMarshal.GetReference(destination);
int count = source.Length;
int index = 0;
// The descending source loads and ascending destination stores never overlap because callers provide distinct
// coefficient and residual spans. The shared index permits a scalar tail for non-vector-sized blocks.
if (Vector512.IsHardwareAccelerated)
{
for (; index <= count - Vector512<int>.Count; index += Vector512<int>.Count)
{
Load512(ref sourceBase, count, index, true).StoreUnsafe(ref destinationBase, (nuint)index);
}
}
if (Vector256.IsHardwareAccelerated)
{
for (; index <= count - Vector256<int>.Count; index += Vector256<int>.Count)
{
Load256(ref sourceBase, count, index, true).StoreUnsafe(ref destinationBase, (nuint)index);
}
}
if (Vector128.IsHardwareAccelerated)
{
for (; index <= count - Vector128<int>.Count; index += Vector128<int>.Count)
{
Load128(ref sourceBase, count, index, true).StoreUnsafe(ref destinationBase, (nuint)index);
}
}
for (; index < count; index++)
{
Unsafe.Add(ref destinationBase, index) = Unsafe.Add(ref sourceBase, count - 1 - index);
}
}
/// <summary>
/// Loads and optionally reverses sixteen source coefficients.
/// </summary>
/// <param name="source">The first source coefficient.</param>
/// <param name="count">The complete coefficient count.</param>
/// <param name="index">The destination coefficient index.</param>
/// <param name="rotate">Whether the complete coefficient order is reversed.</param>
/// <returns>The source coefficients in destination order.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<int> Load512(ref int source, int count, int index, bool rotate)
{
if (!rotate)
{
return Vector512.LoadUnsafe(ref source, (nuint)index);
}
Vector512<int> values = Vector512.LoadUnsafe(ref source, (nuint)(count - index - Vector512<int>.Count));
return Vector512.Shuffle(values, Vector512.Create(15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0));
}
/// <summary>
/// Loads and optionally reverses eight source coefficients.
/// </summary>
/// <param name="source">The first source coefficient.</param>
/// <param name="count">The complete coefficient count.</param>
/// <param name="index">The destination coefficient index.</param>
/// <param name="rotate">Whether the complete coefficient order is reversed.</param>
/// <returns>The source coefficients in destination order.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<int> Load256(ref int source, int count, int index, bool rotate)
{
if (!rotate)
{
return Vector256.LoadUnsafe(ref source, (nuint)index);
}
Vector256<int> values = Vector256.LoadUnsafe(ref source, (nuint)(count - index - Vector256<int>.Count));
return Vector256.Shuffle(values, Vector256.Create(7, 6, 5, 4, 3, 2, 1, 0));
}
/// <summary>
/// Loads and optionally reverses four source coefficients.
/// </summary>
/// <param name="source">The first source coefficient.</param>
/// <param name="count">The complete coefficient count.</param>
/// <param name="index">The destination coefficient index.</param>
/// <param name="rotate">Whether the complete coefficient order is reversed.</param>
/// <returns>The source coefficients in destination order.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<int> Load128(ref int source, int count, int index, bool rotate)
{
if (!rotate)
{
return Vector128.LoadUnsafe(ref source, (nuint)index);
}
Vector128<int> values = Vector128.LoadUnsafe(ref source, (nuint)(count - index - Vector128<int>.Count));
return Vector128.Shuffle(values, Vector128.Create(3, 2, 1, 0));
}
/// <summary>
/// Accumulates residual differences from top to bottom while processing independent columns in SIMD lanes.
/// </summary>
/// <param name="residual">The residual block in packed raster order.</param>
/// <param name="width">The residual-block width.</param>
/// <param name="height">The residual-block height.</param>
private static void ApplyVerticalResidualDpcm(Span<int> residual, int width, int height)
{
ref int residualBase = ref MemoryMarshal.GetReference(residual);
int x = 0;
// Lanes are independent columns. Carrying the reconstructed row above in the accumulator removes the need for
// a horizontal shuffle while preserving the top-to-bottom dependency of residual DPCM.
if (Vector512.IsHardwareAccelerated)
{
Vector512<int> minimum = Vector512.Create(ResidualMinimum);
Vector512<int> maximum = Vector512.Create(ResidualMaximum);
for (; x <= width - Vector512<int>.Count; x += Vector512<int>.Count)
{
Vector512<int> accumulator = Vector512.LoadUnsafe(ref residualBase, (nuint)x);
for (int y = 1; y < height; y++)
{
int index = (y * width) + x;
accumulator += Vector512.LoadUnsafe(ref residualBase, (nuint)index);
Vector512.Clamp(accumulator, minimum, maximum).StoreUnsafe(ref residualBase, (nuint)index);
}
}
}
if (Vector256.IsHardwareAccelerated)
{
Vector256<int> minimum = Vector256.Create(ResidualMinimum);
Vector256<int> maximum = Vector256.Create(ResidualMaximum);
for (; x <= width - Vector256<int>.Count; x += Vector256<int>.Count)
{
Vector256<int> accumulator = Vector256.LoadUnsafe(ref residualBase, (nuint)x);
for (int y = 1; y < height; y++)
{
int index = (y * width) + x;
accumulator += Vector256.LoadUnsafe(ref residualBase, (nuint)index);
Vector256.Clamp(accumulator, minimum, maximum).StoreUnsafe(ref residualBase, (nuint)index);
}
}
}
if (Vector128.IsHardwareAccelerated)
{
Vector128<int> minimum = Vector128.Create(ResidualMinimum);
Vector128<int> maximum = Vector128.Create(ResidualMaximum);
for (; x <= width - Vector128<int>.Count; x += Vector128<int>.Count)
{
Vector128<int> accumulator = Vector128.LoadUnsafe(ref residualBase, (nuint)x);
for (int y = 1; y < height; y++)
{
int index = (y * width) + x;
accumulator += Vector128.LoadUnsafe(ref residualBase, (nuint)index);
Vector128.Clamp(accumulator, minimum, maximum).StoreUnsafe(ref residualBase, (nuint)index);
}
}
}
for (; x < width; x++)
{
int accumulator = Unsafe.Add(ref residualBase, x);
for (int y = 1; y < height; y++)
{
int index = (y * width) + x;
accumulator += Unsafe.Add(ref residualBase, index);
Unsafe.Add(ref residualBase, index) = Math.Clamp(accumulator, ResidualMinimum, ResidualMaximum);
}
}
}
/// <summary>
/// Accumulates residual differences from left to right using an inclusive SIMD prefix sum for each row.
/// </summary>
/// <param name="residual">The residual block in packed raster order.</param>
/// <param name="width">The residual-block width.</param>
/// <param name="height">The residual-block height.</param>
private static void ApplyHorizontalResidualDpcm(Span<int> residual, int width, int height)
{
ref int residualBase = ref MemoryMarshal.GetReference(residual);
for (int y = 0; y < height; y++)
{
int rowOffset = y * width;
int x = 0;
int accumulator = 0;
// PrefixSum resolves dependencies inside a vector. The final lane then seeds the next vector width or the
// scalar tail, so changing SIMD width cannot change the left-to-right accumulation order.
if (Vector512.IsHardwareAccelerated)
{
Vector512<int> minimum = Vector512.Create(ResidualMinimum);
Vector512<int> maximum = Vector512.Create(ResidualMaximum);
for (; x <= width - Vector512<int>.Count; x += Vector512<int>.Count)
{
Vector512<int> values = Vector512.LoadUnsafe(ref residualBase, (nuint)(rowOffset + x));
values = PrefixSum(values) + Vector512.Create(accumulator);
accumulator = values.GetElement(Vector512<int>.Count - 1);
Vector512.Clamp(values, minimum, maximum).StoreUnsafe(ref residualBase, (nuint)(rowOffset + x));
}
}
if (Vector256.IsHardwareAccelerated)
{
Vector256<int> minimum = Vector256.Create(ResidualMinimum);
Vector256<int> maximum = Vector256.Create(ResidualMaximum);
for (; x <= width - Vector256<int>.Count; x += Vector256<int>.Count)
{
Vector256<int> values = Vector256.LoadUnsafe(ref residualBase, (nuint)(rowOffset + x));
values = PrefixSum(values) + Vector256.Create(accumulator);
accumulator = values.GetElement(Vector256<int>.Count - 1);
Vector256.Clamp(values, minimum, maximum).StoreUnsafe(ref residualBase, (nuint)(rowOffset + x));
}
}
if (Vector128.IsHardwareAccelerated)
{
Vector128<int> minimum = Vector128.Create(ResidualMinimum);
Vector128<int> maximum = Vector128.Create(ResidualMaximum);
for (; x <= width - Vector128<int>.Count; x += Vector128<int>.Count)
{
Vector128<int> values = Vector128.LoadUnsafe(ref residualBase, (nuint)(rowOffset + x));
values = PrefixSum(values) + Vector128.Create(accumulator);
accumulator = values.GetElement(Vector128<int>.Count - 1);
Vector128.Clamp(values, minimum, maximum).StoreUnsafe(ref residualBase, (nuint)(rowOffset + x));
}
}
for (; x < width; x++)
{
int index = rowOffset + x;
accumulator += Unsafe.Add(ref residualBase, index);
Unsafe.Add(ref residualBase, index) = x == 0 ? accumulator : Math.Clamp(accumulator, ResidualMinimum, ResidualMaximum);
}
}
}
/// <summary>
/// Computes an inclusive prefix sum across sixteen signed lanes.
/// </summary>
/// <param name="values">The residual differences.</param>
/// <returns>The accumulated residuals.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<int> PrefixSum(Vector512<int> values)
{
// Out-of-range shuffle indices create zero lanes. Distances 1, 2, 4, and 8 form an inclusive Hillis-Steele
// scan without carrying values backward across the start of the vector.
values += Vector512.Shuffle(values, Vector512.Create(16, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14));
values += Vector512.Shuffle(values, Vector512.Create(16, 16, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13));
values += Vector512.Shuffle(values, Vector512.Create(16, 16, 16, 16, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11));
return values + Vector512.Shuffle(values, Vector512.Create(16, 16, 16, 16, 16, 16, 16, 16, 0, 1, 2, 3, 4, 5, 6, 7));
}
/// <summary>
/// Computes an inclusive prefix sum across eight signed lanes.
/// </summary>
/// <param name="values">The residual differences.</param>
/// <returns>The accumulated residuals.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<int> PrefixSum(Vector256<int> values)
{
// Out-of-range index eight supplies the zero lanes needed at each doubling step.
values += Vector256.Shuffle(values, Vector256.Create(8, 0, 1, 2, 3, 4, 5, 6));
values += Vector256.Shuffle(values, Vector256.Create(8, 8, 0, 1, 2, 3, 4, 5));
return values + Vector256.Shuffle(values, Vector256.Create(8, 8, 8, 8, 0, 1, 2, 3));
}
/// <summary>
/// Computes an inclusive prefix sum across four signed lanes.
/// </summary>
/// <param name="values">The residual differences.</param>
/// <returns>The accumulated residuals.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<int> PrefixSum(Vector128<int> values)
{
// Out-of-range index four supplies the zero lanes needed at distances one and two.
values += Vector128.Shuffle(values, Vector128.Create(4, 0, 1, 2));
return values + Vector128.Shuffle(values, Vector128.Create(4, 4, 0, 1));
}
}

51
src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetFilter.BandOperator.cs

@ -1,51 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
internal static partial class HevcSampleAdaptiveOffsetFilter
{
/// <summary>
/// Classifies samples by one of thirty-two most-significant-value bands.
/// </summary>
private readonly struct BandOperator : ISampleClassifier
{
/// <inheritdoc/>
public static bool UsesNeighbors => false;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<short> Classify(
Vector512<short> current,
Vector512<short> neighbor0,
Vector512<short> neighbor1,
in KernelParameters kernel)
=> (Vector512.ShiftRightArithmetic(current, kernel.BandShift) - Vector512.Create(kernel.BandPosition)) & Vector512.Create((short)31);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<short> Classify(
Vector256<short> current,
Vector256<short> neighbor0,
Vector256<short> neighbor1,
in KernelParameters kernel)
=> (Vector256.ShiftRightArithmetic(current, kernel.BandShift) - Vector256.Create(kernel.BandPosition)) & Vector256.Create((short)31);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<short> Classify(
Vector128<short> current,
Vector128<short> neighbor0,
Vector128<short> neighbor1,
in KernelParameters kernel)
=> (Vector128.ShiftRightArithmetic(current, kernel.BandShift) - Vector128.Create(kernel.BandPosition)) & Vector128.Create((short)31);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Classify(short current, short neighbor0, short neighbor1, in KernelParameters kernel)
=> ((current >> kernel.BandShift) - kernel.BandPosition) & 31;
}
}

68
src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetFilter.EdgeOperator.cs

@ -1,68 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
internal static partial class HevcSampleAdaptiveOffsetFilter
{
/// <summary>
/// Classifies samples by the sum of their signs relative to two directional neighbors.
/// </summary>
private readonly struct EdgeOperator : ISampleClassifier
{
/// <inheritdoc/>
public static bool UsesNeighbors => true;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<short> Classify(
Vector512<short> current,
Vector512<short> neighbor0,
Vector512<short> neighbor1,
in KernelParameters kernel)
{
// Each comparison pair produces -1, 0, or 1. Adding two maps the normative edge classes onto the
// contiguous zero-through-four offset-table indices used by the selection kernel.
Vector512<short> one = Vector512.Create((short)1);
Vector512<short> sign0 = (Vector512.GreaterThan(current, neighbor0) & one) - (Vector512.LessThan(current, neighbor0) & one);
Vector512<short> sign1 = (Vector512.GreaterThan(current, neighbor1) & one) - (Vector512.LessThan(current, neighbor1) & one);
return sign0 + sign1 + Vector512.Create((short)2);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<short> Classify(
Vector256<short> current,
Vector256<short> neighbor0,
Vector256<short> neighbor1,
in KernelParameters kernel)
{
Vector256<short> one = Vector256.Create((short)1);
Vector256<short> sign0 = (Vector256.GreaterThan(current, neighbor0) & one) - (Vector256.LessThan(current, neighbor0) & one);
Vector256<short> sign1 = (Vector256.GreaterThan(current, neighbor1) & one) - (Vector256.LessThan(current, neighbor1) & one);
return sign0 + sign1 + Vector256.Create((short)2);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<short> Classify(
Vector128<short> current,
Vector128<short> neighbor0,
Vector128<short> neighbor1,
in KernelParameters kernel)
{
Vector128<short> one = Vector128.Create((short)1);
Vector128<short> sign0 = (Vector128.GreaterThan(current, neighbor0) & one) - (Vector128.LessThan(current, neighbor0) & one);
Vector128<short> sign1 = (Vector128.GreaterThan(current, neighbor1) & one) - (Vector128.LessThan(current, neighbor1) & one);
return sign0 + sign1 + Vector128.Create((short)2);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Classify(short current, short neighbor0, short neighbor1, in KernelParameters kernel)
=> Math.Sign(current - neighbor0) + Math.Sign(current - neighbor1) + 2;
}
}

72
src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetFilter.Operator.cs

@ -1,72 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
internal static partial class HevcSampleAdaptiveOffsetFilter
{
/// <summary>
/// Defines the sample classifier shared by the SIMD row traversal and scalar tail.
/// </summary>
private interface ISampleClassifier
{
/// <summary>
/// Gets a value indicating whether classification reads the two neighboring sample rows.
/// </summary>
public static abstract bool UsesNeighbors { get; }
/// <summary>
/// Classifies thirty-two current samples against their two classifier inputs.
/// </summary>
/// <param name="current">The current sample lanes.</param>
/// <param name="neighbor0">The first neighboring sample lanes.</param>
/// <param name="neighbor1">The second neighboring sample lanes.</param>
/// <param name="kernel">The scaled offset and band-class state.</param>
/// <returns>The zero-based offset-table indices.</returns>
public static abstract Vector512<short> Classify(
Vector512<short> current,
Vector512<short> neighbor0,
Vector512<short> neighbor1,
in KernelParameters kernel);
/// <summary>
/// Classifies sixteen current samples against their two classifier inputs.
/// </summary>
/// <param name="current">The current sample lanes.</param>
/// <param name="neighbor0">The first neighboring sample lanes.</param>
/// <param name="neighbor1">The second neighboring sample lanes.</param>
/// <param name="kernel">The scaled offset and band-class state.</param>
/// <returns>The zero-based offset-table indices.</returns>
public static abstract Vector256<short> Classify(
Vector256<short> current,
Vector256<short> neighbor0,
Vector256<short> neighbor1,
in KernelParameters kernel);
/// <summary>
/// Classifies eight current samples against their two classifier inputs.
/// </summary>
/// <param name="current">The current sample lanes.</param>
/// <param name="neighbor0">The first neighboring sample lanes.</param>
/// <param name="neighbor1">The second neighboring sample lanes.</param>
/// <param name="kernel">The scaled offset and band-class state.</param>
/// <returns>The zero-based offset-table indices.</returns>
public static abstract Vector128<short> Classify(
Vector128<short> current,
Vector128<short> neighbor0,
Vector128<short> neighbor1,
in KernelParameters kernel);
/// <summary>
/// Classifies one current sample against its two classifier inputs.
/// </summary>
/// <param name="current">The current sample.</param>
/// <param name="neighbor0">The first neighboring sample.</param>
/// <param name="neighbor1">The second neighboring sample.</param>
/// <param name="kernel">The scaled offset and band-class state.</param>
/// <returns>The zero-based offset-table index.</returns>
public static abstract int Classify(short current, short neighbor0, short neighbor1, in KernelParameters kernel);
}
}

581
src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetFilter.cs

@ -1,581 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Applies HEVC sample-adaptive offsets to reconstructed component blocks.
/// </summary>
/// <remarks>
/// Each SIMD lane classifies one reconstructed sample. Band-offset operators derive the class directly from the current
/// value, while edge-offset operators compare aligned lanes from the two neighboring coordinates. The resulting class
/// indices select one of the signaled offsets, after which addition and bit-depth clipping remain lane-wise. A scalar
/// continuation handles only incomplete vectors at picture edges.
/// </remarks>
internal static partial class HevcSampleAdaptiveOffsetFilter
{
/// <summary>
/// Applies one resolved sample-adaptive-offset mode to a component coding-tree block.
/// </summary>
/// <param name="source">The immutable pre-SAO picture used for every classification.</param>
/// <param name="destination">The picture receiving filtered samples.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The block's left coordinate in component samples.</param>
/// <param name="y">The block's top coordinate in component samples.</param>
/// <param name="width">The block width in component samples.</param>
/// <param name="height">The block height in component samples.</param>
/// <param name="parameters">The resolved coded offsets and classifier.</param>
/// <param name="offsetScaleLog2">The component offset scale from the picture range-extension parameters.</param>
/// <param name="leftAvailable">Whether classification may read the block immediately to the left.</param>
/// <param name="rightAvailable">Whether classification may read the block immediately to the right.</param>
/// <param name="aboveAvailable">Whether classification may read the block immediately above.</param>
/// <param name="belowAvailable">Whether classification may read the block immediately below.</param>
/// <param name="aboveLeftAvailable">Whether classification may read the upper-left diagonal block.</param>
/// <param name="aboveRightAvailable">Whether classification may read the upper-right diagonal block.</param>
/// <param name="belowLeftAvailable">Whether classification may read the lower-left diagonal block.</param>
/// <param name="belowRightAvailable">Whether classification may read the lower-right diagonal block.</param>
public static void ApplyBlock(
HevcPictureBuffer source,
HevcPictureBuffer destination,
HevcPlane plane,
int x,
int y,
int width,
int height,
in HevcSampleAdaptiveOffsetParameters parameters,
int offsetScaleLog2,
bool leftAvailable,
bool rightAvailable,
bool aboveAvailable,
bool belowAvailable,
bool aboveLeftAvailable,
bool aboveRightAvailable,
bool belowLeftAvailable,
bool belowRightAvailable)
{
if (parameters.Type == HevcSampleAdaptiveOffsetType.Off)
{
return;
}
KernelParameters kernel = new(parameters, source.GetBitDepth(plane), offsetScaleLog2);
switch (parameters.Type)
{
case HevcSampleAdaptiveOffsetType.Band:
ApplyBand(source, destination, plane, x, y, width, height, in kernel);
break;
case HevcSampleAdaptiveOffsetType.EdgeHorizontal:
ApplyHorizontalEdges(source, destination, plane, x, y, width, height, leftAvailable, rightAvailable, in kernel);
break;
case HevcSampleAdaptiveOffsetType.EdgeVertical:
ApplyVerticalEdges(source, destination, plane, x, y, width, height, aboveAvailable, belowAvailable, in kernel);
break;
case HevcSampleAdaptiveOffsetType.EdgeDescending:
ApplyDescendingEdges(
source,
destination,
plane,
x,
y,
width,
height,
leftAvailable,
rightAvailable,
aboveAvailable,
belowAvailable,
aboveLeftAvailable,
belowRightAvailable,
in kernel);
break;
case HevcSampleAdaptiveOffsetType.EdgeAscending:
ApplyAscendingEdges(
source,
destination,
plane,
x,
y,
width,
height,
leftAvailable,
rightAvailable,
aboveAvailable,
belowAvailable,
aboveRightAvailable,
belowLeftAvailable,
in kernel);
break;
}
}
/// <summary>
/// Applies band offsets to every sample in a component block.
/// </summary>
/// <param name="source">The immutable pre-SAO picture.</param>
/// <param name="destination">The destination picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The block's left coordinate.</param>
/// <param name="y">The block's top coordinate.</param>
/// <param name="width">The block width.</param>
/// <param name="height">The block height.</param>
/// <param name="kernel">The scaled offset and band-class state.</param>
private static void ApplyBand(
HevcPictureBuffer source,
HevcPictureBuffer destination,
HevcPlane plane,
int x,
int y,
int width,
int height,
in KernelParameters kernel)
{
for (int row = y; row < y + height; row++)
{
ReadOnlySpan<ushort> sourceRow = source.GetRowSpan(plane, row).Slice(x, width);
Span<ushort> destinationRow = destination.GetRowSpan(plane, row).Slice(x, width);
// Band classification depends only on the current sample. The closed classifier's UsesNeighbors value removes
// the two neighbor loads when this generic traversal is specialized for BandOperator.
ApplyRow<BandOperator>(sourceRow, sourceRow, sourceRow, destinationRow, in kernel);
}
}
/// <summary>
/// Applies horizontal edge offsets within the available left and right boundaries.
/// </summary>
/// <param name="source">The immutable pre-SAO picture.</param>
/// <param name="destination">The destination picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The block's left coordinate.</param>
/// <param name="y">The block's top coordinate.</param>
/// <param name="width">The block width.</param>
/// <param name="height">The block height.</param>
/// <param name="leftAvailable">Whether the left neighboring block is available.</param>
/// <param name="rightAvailable">Whether the right neighboring block is available.</param>
/// <param name="kernel">The scaled offset state.</param>
private static void ApplyHorizontalEdges(
HevcPictureBuffer source,
HevcPictureBuffer destination,
HevcPlane plane,
int x,
int y,
int width,
int height,
bool leftAvailable,
bool rightAvailable,
in KernelParameters kernel)
{
int start = x + (leftAvailable ? 0 : 1);
int end = x + width - (rightAvailable ? 0 : 1);
int count = end - start;
if (count <= 0)
{
return;
}
for (int row = y; row < y + height; row++)
{
ReadOnlySpan<ushort> sourceRow = source.GetRowSpan(plane, row);
ApplyRow<EdgeOperator>(
sourceRow.Slice(start, count),
sourceRow.Slice(start - 1, count),
sourceRow.Slice(start + 1, count),
destination.GetRowSpan(plane, row).Slice(start, count),
in kernel);
}
}
/// <summary>
/// Applies vertical edge offsets within the available upper and lower boundaries.
/// </summary>
/// <param name="source">The immutable pre-SAO picture.</param>
/// <param name="destination">The destination picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The block's left coordinate.</param>
/// <param name="y">The block's top coordinate.</param>
/// <param name="width">The block width.</param>
/// <param name="height">The block height.</param>
/// <param name="aboveAvailable">Whether the upper neighboring block is available.</param>
/// <param name="belowAvailable">Whether the lower neighboring block is available.</param>
/// <param name="kernel">The scaled offset state.</param>
private static void ApplyVerticalEdges(
HevcPictureBuffer source,
HevcPictureBuffer destination,
HevcPlane plane,
int x,
int y,
int width,
int height,
bool aboveAvailable,
bool belowAvailable,
in KernelParameters kernel)
{
int start = y + (aboveAvailable ? 0 : 1);
int end = y + height - (belowAvailable ? 0 : 1);
for (int row = start; row < end; row++)
{
ApplyRow<EdgeOperator>(
source.GetRowSpan(plane, row).Slice(x, width),
source.GetRowSpan(plane, row - 1).Slice(x, width),
source.GetRowSpan(plane, row + 1).Slice(x, width),
destination.GetRowSpan(plane, row).Slice(x, width),
in kernel);
}
}
/// <summary>
/// Applies descending-diagonal edge offsets within the eight resolved block boundaries.
/// </summary>
/// <param name="source">The immutable pre-SAO picture.</param>
/// <param name="destination">The destination picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The block's left coordinate.</param>
/// <param name="y">The block's top coordinate.</param>
/// <param name="width">The block width.</param>
/// <param name="height">The block height.</param>
/// <param name="leftAvailable">Whether the left neighboring block is available.</param>
/// <param name="rightAvailable">Whether the right neighboring block is available.</param>
/// <param name="aboveAvailable">Whether the upper neighboring block is available.</param>
/// <param name="belowAvailable">Whether the lower neighboring block is available.</param>
/// <param name="aboveLeftAvailable">Whether the upper-left neighboring block is available.</param>
/// <param name="belowRightAvailable">Whether the lower-right neighboring block is available.</param>
/// <param name="kernel">The scaled offset state.</param>
private static void ApplyDescendingEdges(
HevcPictureBuffer source,
HevcPictureBuffer destination,
HevcPlane plane,
int x,
int y,
int width,
int height,
bool leftAvailable,
bool rightAvailable,
bool aboveAvailable,
bool belowAvailable,
bool aboveLeftAvailable,
bool belowRightAvailable,
in KernelParameters kernel)
{
int commonStart = x + (leftAvailable ? 0 : 1);
int commonEnd = x + width - (rightAvailable ? 0 : 1);
int lastRow = y + height - 1;
for (int row = y; row <= lastRow; row++)
{
int start = commonStart;
int end = commonEnd;
if (row == y)
{
start = aboveLeftAvailable ? x : x + 1;
end = aboveAvailable ? commonEnd : x + 1;
}
if (row == lastRow)
{
start = Math.Max(start, belowAvailable ? commonStart : x + width - 1);
end = Math.Min(end, belowRightAvailable ? x + width : x + width - 1);
}
int count = end - start;
if (count <= 0)
{
continue;
}
ApplyRow<EdgeOperator>(
source.GetRowSpan(plane, row).Slice(start, count),
source.GetRowSpan(plane, row - 1).Slice(start - 1, count),
source.GetRowSpan(plane, row + 1).Slice(start + 1, count),
destination.GetRowSpan(plane, row).Slice(start, count),
in kernel);
}
}
/// <summary>
/// Applies ascending-diagonal edge offsets within the eight resolved block boundaries.
/// </summary>
/// <param name="source">The immutable pre-SAO picture.</param>
/// <param name="destination">The destination picture.</param>
/// <param name="plane">The component plane.</param>
/// <param name="x">The block's left coordinate.</param>
/// <param name="y">The block's top coordinate.</param>
/// <param name="width">The block width.</param>
/// <param name="height">The block height.</param>
/// <param name="leftAvailable">Whether the left neighboring block is available.</param>
/// <param name="rightAvailable">Whether the right neighboring block is available.</param>
/// <param name="aboveAvailable">Whether the upper neighboring block is available.</param>
/// <param name="belowAvailable">Whether the lower neighboring block is available.</param>
/// <param name="aboveRightAvailable">Whether the upper-right neighboring block is available.</param>
/// <param name="belowLeftAvailable">Whether the lower-left neighboring block is available.</param>
/// <param name="kernel">The scaled offset state.</param>
private static void ApplyAscendingEdges(
HevcPictureBuffer source,
HevcPictureBuffer destination,
HevcPlane plane,
int x,
int y,
int width,
int height,
bool leftAvailable,
bool rightAvailable,
bool aboveAvailable,
bool belowAvailable,
bool aboveRightAvailable,
bool belowLeftAvailable,
in KernelParameters kernel)
{
int commonStart = x + (leftAvailable ? 0 : 1);
int commonEnd = x + width - (rightAvailable ? 0 : 1);
int lastRow = y + height - 1;
for (int row = y; row <= lastRow; row++)
{
int start = commonStart;
int end = commonEnd;
if (row == y)
{
start = aboveAvailable ? commonStart : x + width - 1;
end = aboveRightAvailable ? x + width : x + width - 1;
}
if (row == lastRow)
{
start = Math.Max(start, belowLeftAvailable ? x : x + 1);
end = Math.Min(end, belowAvailable ? commonEnd : x + 1);
}
int count = end - start;
if (count <= 0)
{
continue;
}
ApplyRow<EdgeOperator>(
source.GetRowSpan(plane, row).Slice(start, count),
source.GetRowSpan(plane, row - 1).Slice(start + 1, count),
source.GetRowSpan(plane, row + 1).Slice(start - 1, count),
destination.GetRowSpan(plane, row).Slice(start, count),
in kernel);
}
}
/// <summary>
/// Applies one closed classifier to a contiguous row range using every accelerated SIMD width before the scalar tail.
/// </summary>
/// <typeparam name="TClassifier">The band or edge classifier selected before entering the row.</typeparam>
/// <param name="current">The current source samples.</param>
/// <param name="neighbor0">The first classifier input samples.</param>
/// <param name="neighbor1">The second classifier input samples.</param>
/// <param name="destination">The destination samples.</param>
/// <param name="kernel">The scaled offset and clamp state.</param>
private static void ApplyRow<TClassifier>(
ReadOnlySpan<ushort> current,
ReadOnlySpan<ushort> neighbor0,
ReadOnlySpan<ushort> neighbor1,
Span<ushort> destination,
in KernelParameters kernel)
where TClassifier : struct, ISampleClassifier
{
ref ushort currentBase = ref MemoryMarshal.GetReference(current);
ref ushort neighbor0Base = ref MemoryMarshal.GetReference(neighbor0);
ref ushort neighbor1Base = ref MemoryMarshal.GetReference(neighbor1);
ref ushort destinationBase = ref MemoryMarshal.GetReference(destination);
int index = 0;
// HEVC's exposed 8/10/12-bit profiles keep every sample and scaled offset inside Int16. Signed lanes therefore
// provide comparisons, addition, and saturation without the two widening stages an Int32 implementation needs.
if (Vector512.IsHardwareAccelerated)
{
Vector512<short> minimum = Vector512<short>.Zero;
Vector512<short> maximum = Vector512.Create(kernel.Maximum);
for (; index <= current.Length - Vector512<ushort>.Count; index += Vector512<ushort>.Count)
{
Vector512<short> value = Vector512.LoadUnsafe(ref currentBase, (nuint)index).AsInt16();
Vector512<short> first = TClassifier.UsesNeighbors ? Vector512.LoadUnsafe(ref neighbor0Base, (nuint)index).AsInt16() : default;
Vector512<short> second = TClassifier.UsesNeighbors ? Vector512.LoadUnsafe(ref neighbor1Base, (nuint)index).AsInt16() : default;
Vector512<short> classes = TClassifier.Classify(value, first, second, in kernel);
Vector512<short> filtered = Vector512.Clamp(value + SelectOffset(classes, in kernel), minimum, maximum);
filtered.AsUInt16().StoreUnsafe(ref destinationBase, (nuint)index);
}
}
if (Vector256.IsHardwareAccelerated)
{
Vector256<short> minimum = Vector256<short>.Zero;
Vector256<short> maximum = Vector256.Create(kernel.Maximum);
for (; index <= current.Length - Vector256<ushort>.Count; index += Vector256<ushort>.Count)
{
Vector256<short> value = Vector256.LoadUnsafe(ref currentBase, (nuint)index).AsInt16();
Vector256<short> first = TClassifier.UsesNeighbors ? Vector256.LoadUnsafe(ref neighbor0Base, (nuint)index).AsInt16() : default;
Vector256<short> second = TClassifier.UsesNeighbors ? Vector256.LoadUnsafe(ref neighbor1Base, (nuint)index).AsInt16() : default;
Vector256<short> classes = TClassifier.Classify(value, first, second, in kernel);
Vector256<short> filtered = Vector256.Clamp(value + SelectOffset(classes, in kernel), minimum, maximum);
filtered.AsUInt16().StoreUnsafe(ref destinationBase, (nuint)index);
}
}
if (Vector128.IsHardwareAccelerated)
{
Vector128<short> minimum = Vector128<short>.Zero;
Vector128<short> maximum = Vector128.Create(kernel.Maximum);
for (; index <= current.Length - Vector128<ushort>.Count; index += Vector128<ushort>.Count)
{
Vector128<short> value = Vector128.LoadUnsafe(ref currentBase, (nuint)index).AsInt16();
Vector128<short> first = TClassifier.UsesNeighbors ? Vector128.LoadUnsafe(ref neighbor0Base, (nuint)index).AsInt16() : default;
Vector128<short> second = TClassifier.UsesNeighbors ? Vector128.LoadUnsafe(ref neighbor1Base, (nuint)index).AsInt16() : default;
Vector128<short> classes = TClassifier.Classify(value, first, second, in kernel);
Vector128<short> filtered = Vector128.Clamp(value + SelectOffset(classes, in kernel), minimum, maximum);
filtered.AsUInt16().StoreUnsafe(ref destinationBase, (nuint)index);
}
}
for (; index < current.Length; index++)
{
short currentValue = (short)Unsafe.Add(ref currentBase, index);
short first = TClassifier.UsesNeighbors ? (short)Unsafe.Add(ref neighbor0Base, index) : default;
short second = TClassifier.UsesNeighbors ? (short)Unsafe.Add(ref neighbor1Base, index) : default;
int offsetIndex = TClassifier.Classify(currentValue, first, second, in kernel);
int filtered = Unsafe.Add(ref currentBase, index) + SelectOffset(offsetIndex, in kernel);
Unsafe.Add(ref destinationBase, index) = (ushort)Math.Clamp(filtered, 0, kernel.Maximum);
}
}
/// <summary>
/// Selects one of five signed offsets for thirty-two classifier indices.
/// </summary>
/// <param name="classes">The zero-based classifier indices.</param>
/// <param name="kernel">The five scaled offsets.</param>
/// <returns>The selected signed offset in every lane.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<short> SelectOffset(Vector512<short> classes, in KernelParameters kernel)
{
// The table contains only five values and there is no portable 16-bit gather. A comparison chain keeps every
// class lane in registers and leaves unrecognized classes at the required zero offset.
Vector512<short> selected = Vector512<short>.Zero;
selected = Vector512.ConditionalSelect(Vector512.Equals(classes, Vector512.Create((short)0)), Vector512.Create(kernel.Offset0), selected);
selected = Vector512.ConditionalSelect(Vector512.Equals(classes, Vector512.Create((short)1)), Vector512.Create(kernel.Offset1), selected);
selected = Vector512.ConditionalSelect(Vector512.Equals(classes, Vector512.Create((short)2)), Vector512.Create(kernel.Offset2), selected);
selected = Vector512.ConditionalSelect(Vector512.Equals(classes, Vector512.Create((short)3)), Vector512.Create(kernel.Offset3), selected);
return Vector512.ConditionalSelect(Vector512.Equals(classes, Vector512.Create((short)4)), Vector512.Create(kernel.Offset4), selected);
}
/// <summary>
/// Selects one of five signed offsets for sixteen classifier indices.
/// </summary>
/// <param name="classes">The zero-based classifier indices.</param>
/// <param name="kernel">The five scaled offsets.</param>
/// <returns>The selected signed offset in every lane.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<short> SelectOffset(Vector256<short> classes, in KernelParameters kernel)
{
Vector256<short> selected = Vector256<short>.Zero;
selected = Vector256.ConditionalSelect(Vector256.Equals(classes, Vector256.Create((short)0)), Vector256.Create(kernel.Offset0), selected);
selected = Vector256.ConditionalSelect(Vector256.Equals(classes, Vector256.Create((short)1)), Vector256.Create(kernel.Offset1), selected);
selected = Vector256.ConditionalSelect(Vector256.Equals(classes, Vector256.Create((short)2)), Vector256.Create(kernel.Offset2), selected);
selected = Vector256.ConditionalSelect(Vector256.Equals(classes, Vector256.Create((short)3)), Vector256.Create(kernel.Offset3), selected);
return Vector256.ConditionalSelect(Vector256.Equals(classes, Vector256.Create((short)4)), Vector256.Create(kernel.Offset4), selected);
}
/// <summary>
/// Selects one of five signed offsets for eight classifier indices.
/// </summary>
/// <param name="classes">The zero-based classifier indices.</param>
/// <param name="kernel">The five scaled offsets.</param>
/// <returns>The selected signed offset in every lane.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<short> SelectOffset(Vector128<short> classes, in KernelParameters kernel)
{
Vector128<short> selected = Vector128<short>.Zero;
selected = Vector128.ConditionalSelect(Vector128.Equals(classes, Vector128.Create((short)0)), Vector128.Create(kernel.Offset0), selected);
selected = Vector128.ConditionalSelect(Vector128.Equals(classes, Vector128.Create((short)1)), Vector128.Create(kernel.Offset1), selected);
selected = Vector128.ConditionalSelect(Vector128.Equals(classes, Vector128.Create((short)2)), Vector128.Create(kernel.Offset2), selected);
selected = Vector128.ConditionalSelect(Vector128.Equals(classes, Vector128.Create((short)3)), Vector128.Create(kernel.Offset3), selected);
return Vector128.ConditionalSelect(Vector128.Equals(classes, Vector128.Create((short)4)), Vector128.Create(kernel.Offset4), selected);
}
/// <summary>
/// Selects one of five signed offsets for one classifier index.
/// </summary>
/// <param name="classification">The zero-based classifier index.</param>
/// <param name="kernel">The five scaled offsets.</param>
/// <returns>The selected signed offset, or zero for an unmodified class.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int SelectOffset(int classification, in KernelParameters kernel)
=> classification switch
{
0 => kernel.Offset0,
1 => kernel.Offset1,
2 => kernel.Offset2,
3 => kernel.Offset3,
4 => kernel.Offset4,
_ => 0,
};
/// <summary>
/// Contains one block's scaled offsets and invariant classification values.
/// </summary>
private readonly struct KernelParameters
{
/// <summary>
/// Initializes a new instance of the <see cref="KernelParameters"/> struct.
/// </summary>
/// <param name="parameters">The decoded signed offsets.</param>
/// <param name="bitDepth">The component sample precision.</param>
/// <param name="offsetScaleLog2">The component offset scale.</param>
public KernelParameters(in HevcSampleAdaptiveOffsetParameters parameters, int bitDepth, int offsetScaleLog2)
{
// Range Extensions scales each coded offset once before filtering. Hoisting the shifts here keeps the
// classification loops to comparisons, table selection, one addition, and saturation.
this.Offset0 = (short)(parameters.Offset0 << offsetScaleLog2);
this.Offset1 = (short)(parameters.Offset1 << offsetScaleLog2);
this.Offset2 = (short)(parameters.Offset2 << offsetScaleLog2);
this.Offset3 = (short)(parameters.Offset3 << offsetScaleLog2);
this.Offset4 = (short)(parameters.Offset4 << offsetScaleLog2);
this.BandPosition = (short)parameters.BandPosition;
this.BandShift = bitDepth - 5;
this.Maximum = (short)((1 << bitDepth) - 1);
}
/// <summary>
/// Gets the first scaled class offset.
/// </summary>
public short Offset0 { get; }
/// <summary>
/// Gets the second scaled class offset.
/// </summary>
public short Offset1 { get; }
/// <summary>
/// Gets the third scaled class offset.
/// </summary>
public short Offset2 { get; }
/// <summary>
/// Gets the fourth scaled class offset.
/// </summary>
public short Offset3 { get; }
/// <summary>
/// Gets the fifth scaled class offset.
/// </summary>
public short Offset4 { get; }
/// <summary>
/// Gets the first active band class.
/// </summary>
public short BandPosition { get; }
/// <summary>
/// Gets the number of low sample bits discarded to form one of thirty-two band classes.
/// </summary>
public int BandShift { get; }
/// <summary>
/// Gets the largest component sample value.
/// </summary>
public short Maximum { get; }
}
}

464
src/ImageSharp/Formats/Heif/Hevc/HevcSampleAdaptiveOffsetParameters.cs

@ -1,464 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Identifies the HEVC sample-adaptive-offset classifier selected for one component coding-tree block.
/// </summary>
internal enum HevcSampleAdaptiveOffsetType : byte
{
/// <summary>
/// No sample-adaptive offset is applied.
/// </summary>
Off,
/// <summary>
/// Samples are classified by their most-significant sample-value band.
/// </summary>
Band,
/// <summary>
/// Samples are classified by horizontal neighboring samples.
/// </summary>
EdgeHorizontal,
/// <summary>
/// Samples are classified by vertical neighboring samples.
/// </summary>
EdgeVertical,
/// <summary>
/// Samples are classified by neighbors on the descending diagonal.
/// </summary>
EdgeDescending,
/// <summary>
/// Samples are classified by neighbors on the ascending diagonal.
/// </summary>
EdgeAscending,
}
/// <summary>
/// Contains the resolved HEVC sample-adaptive offsets for one component coding-tree block.
/// </summary>
internal readonly struct HevcSampleAdaptiveOffsetParameters
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcSampleAdaptiveOffsetParameters"/> struct.
/// </summary>
/// <param name="type">The sample classifier.</param>
/// <param name="bandPosition">The first of four consecutive band classes.</param>
/// <param name="offset0">The first band or full-valley offset.</param>
/// <param name="offset1">The second band or half-valley offset.</param>
/// <param name="offset2">The third band or plain-edge offset.</param>
/// <param name="offset3">The fourth band or half-peak offset.</param>
/// <param name="offset4">The full-peak offset.</param>
public HevcSampleAdaptiveOffsetParameters(
HevcSampleAdaptiveOffsetType type,
int bandPosition,
int offset0,
int offset1,
int offset2,
int offset3,
int offset4)
{
this.Type = type;
this.BandPosition = bandPosition;
this.Offset0 = offset0;
this.Offset1 = offset1;
this.Offset2 = offset2;
this.Offset3 = offset3;
this.Offset4 = offset4;
}
/// <summary>
/// Gets the sample classifier.
/// </summary>
public HevcSampleAdaptiveOffsetType Type { get; }
/// <summary>
/// Gets the first of four consecutive band classes.
/// </summary>
public int BandPosition { get; }
/// <summary>
/// Gets the first band or full-valley offset.
/// </summary>
public int Offset0 { get; }
/// <summary>
/// Gets the second band or half-valley offset.
/// </summary>
public int Offset1 { get; }
/// <summary>
/// Gets the third band or plain-edge offset.
/// </summary>
public int Offset2 { get; }
/// <summary>
/// Gets the fourth band or half-peak offset.
/// </summary>
public int Offset3 { get; }
/// <summary>
/// Gets the full-peak offset.
/// </summary>
public int Offset4 { get; }
}
/// <summary>
/// Identifies the slice and tile governing in-loop filtering for one coding-tree block.
/// </summary>
internal readonly struct HevcLoopFilterRegion
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcLoopFilterRegion"/> struct.
/// </summary>
/// <param name="sliceStartAddressInTileScan">The first coding-tree block of the independent slice in tile-scan order.</param>
/// <param name="tileIndex">The zero-based tile index.</param>
/// <param name="loopFilterAcrossSlicesEnabled">Whether the governing slice permits filtering across its slice boundary.</param>
/// <param name="deblockingFilterDisabled">Whether the governing slice disables deblocking.</param>
/// <param name="deblockingFilterBetaOffsetDiv2">Half the slice beta-threshold offset.</param>
/// <param name="deblockingFilterTcOffsetDiv2">Half the slice clipping-threshold offset.</param>
public HevcLoopFilterRegion(
int sliceStartAddressInTileScan,
int tileIndex,
bool loopFilterAcrossSlicesEnabled,
bool deblockingFilterDisabled,
int deblockingFilterBetaOffsetDiv2,
int deblockingFilterTcOffsetDiv2)
{
this.SliceStartAddressInTileScan = sliceStartAddressInTileScan;
this.TileIndex = tileIndex;
this.LoopFilterAcrossSlicesEnabled = loopFilterAcrossSlicesEnabled;
this.DeblockingFilterDisabled = deblockingFilterDisabled;
this.DeblockingFilterBetaOffsetDiv2 = deblockingFilterBetaOffsetDiv2;
this.DeblockingFilterTcOffsetDiv2 = deblockingFilterTcOffsetDiv2;
}
/// <summary>
/// Gets the first coding-tree block of the independent slice in tile-scan order.
/// </summary>
public int SliceStartAddressInTileScan { get; }
/// <summary>
/// Gets the zero-based tile index.
/// </summary>
public int TileIndex { get; }
/// <summary>
/// Gets a value indicating whether the governing slice permits filtering across its slice boundary.
/// </summary>
public bool LoopFilterAcrossSlicesEnabled { get; }
/// <summary>
/// Gets a value indicating whether the governing slice disables deblocking.
/// </summary>
public bool DeblockingFilterDisabled { get; }
/// <summary>
/// Gets half the governing slice's beta-threshold offset.
/// </summary>
public int DeblockingFilterBetaOffsetDiv2 { get; }
/// <summary>
/// Gets half the governing slice's clipping-threshold offset.
/// </summary>
public int DeblockingFilterTcOffsetDiv2 { get; }
}
/// <summary>
/// Contains the eight coding-tree-block neighbor availability values used by HEVC in-loop filters.
/// </summary>
internal readonly struct HevcLoopFilterBoundaryAvailability
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcLoopFilterBoundaryAvailability"/> struct.
/// </summary>
/// <param name="left">Whether the left block is available.</param>
/// <param name="right">Whether the right block is available.</param>
/// <param name="above">Whether the block above is available.</param>
/// <param name="below">Whether the block below is available.</param>
/// <param name="aboveLeft">Whether the upper-left block is available.</param>
/// <param name="aboveRight">Whether the upper-right block is available.</param>
/// <param name="belowLeft">Whether the lower-left block is available.</param>
/// <param name="belowRight">Whether the lower-right block is available.</param>
public HevcLoopFilterBoundaryAvailability(
bool left,
bool right,
bool above,
bool below,
bool aboveLeft,
bool aboveRight,
bool belowLeft,
bool belowRight)
{
this.Left = left;
this.Right = right;
this.Above = above;
this.Below = below;
this.AboveLeft = aboveLeft;
this.AboveRight = aboveRight;
this.BelowLeft = belowLeft;
this.BelowRight = belowRight;
}
/// <summary>
/// Gets a value indicating whether the left block is available.
/// </summary>
public bool Left { get; }
/// <summary>
/// Gets a value indicating whether the right block is available.
/// </summary>
public bool Right { get; }
/// <summary>
/// Gets a value indicating whether the block above is available.
/// </summary>
public bool Above { get; }
/// <summary>
/// Gets a value indicating whether the block below is available.
/// </summary>
public bool Below { get; }
/// <summary>
/// Gets a value indicating whether the upper-left block is available.
/// </summary>
public bool AboveLeft { get; }
/// <summary>
/// Gets a value indicating whether the upper-right block is available.
/// </summary>
public bool AboveRight { get; }
/// <summary>
/// Gets a value indicating whether the lower-left block is available.
/// </summary>
public bool BelowLeft { get; }
/// <summary>
/// Gets a value indicating whether the lower-right block is available.
/// </summary>
public bool BelowRight { get; }
}
/// <summary>
/// Owns resolved sample-adaptive-offset parameters and prediction and filter region identifiers for one picture.
/// </summary>
internal sealed class HevcSampleAdaptiveOffsetState : IDisposable
{
/// <summary>
/// Whether any decoded component block enables sample-adaptive offset.
/// </summary>
private bool hasEnabledParameters;
/// <summary>
/// The three component records for every raster-ordered coding-tree block.
/// </summary>
private readonly IMemoryOwner<HevcSampleAdaptiveOffsetParameters> parameters;
/// <summary>
/// The independent-slice and tile prediction region of every coding-tree block.
/// </summary>
private readonly IMemoryOwner<int> regions;
/// <summary>
/// The independent-slice and tile filter region of every coding-tree block and color plane.
/// </summary>
private readonly IMemoryOwner<HevcLoopFilterRegion> loopFilterRegions;
/// <summary>
/// Initializes a new instance of the <see cref="HevcSampleAdaptiveOffsetState"/> class.
/// </summary>
/// <param name="configuration">The configuration providing pooled picture state.</param>
/// <param name="codingTreeBlockCount">The raster-ordered coding-tree-block count.</param>
public HevcSampleAdaptiveOffsetState(Configuration configuration, int codingTreeBlockCount)
{
IMemoryOwner<HevcSampleAdaptiveOffsetParameters>? parameters = null;
IMemoryOwner<int>? regions = null;
IMemoryOwner<HevcLoopFilterRegion>? loopFilterRegions = null;
try
{
parameters = configuration.MemoryAllocator.Allocate<HevcSampleAdaptiveOffsetParameters>(codingTreeBlockCount * 3);
// Slice headers can disable SAO independently for luma and chroma. Initialize every component record to Off
// so an enabled component never causes untouched records from pooled memory to enter the picture-level pass.
parameters.Memory.Span.Clear();
regions = configuration.MemoryAllocator.Allocate<int>(codingTreeBlockCount * 3);
loopFilterRegions = configuration.MemoryAllocator.Allocate<HevcLoopFilterRegion>(codingTreeBlockCount * 3);
this.parameters = parameters;
this.regions = regions;
this.loopFilterRegions = loopFilterRegions;
}
catch
{
loopFilterRegions?.Dispose();
regions?.Dispose();
parameters?.Dispose();
throw;
}
}
/// <summary>
/// Gets a value indicating whether any component block enables sample-adaptive offset.
/// </summary>
public bool HasEnabledParameters => this.hasEnabledParameters;
/// <summary>
/// Gets the resolved component parameters for one coding-tree block.
/// </summary>
/// <param name="rasterAddress">The raster-scan coding-tree-block address.</param>
/// <param name="plane">The component plane.</param>
/// <returns>The resolved sample-adaptive-offset parameters.</returns>
public HevcSampleAdaptiveOffsetParameters Get(int rasterAddress, HevcPlane plane)
=> this.parameters.Memory.Span[(rasterAddress * 3) + (int)plane];
/// <summary>
/// Stores resolved component parameters for one coding-tree block.
/// </summary>
/// <param name="rasterAddress">The raster-scan coding-tree-block address.</param>
/// <param name="plane">The component plane.</param>
/// <param name="value">The resolved sample-adaptive-offset parameters.</param>
public void Set(int rasterAddress, HevcPlane plane, HevcSampleAdaptiveOffsetParameters value)
{
this.parameters.Memory.Span[(rasterAddress * 3) + (int)plane] = value;
this.hasEnabledParameters |= value.Type != HevcSampleAdaptiveOffsetType.Off;
}
/// <summary>
/// Gets whether one coding-tree block belongs to the selected prediction region.
/// </summary>
/// <param name="rasterAddress">The raster-scan coding-tree-block address.</param>
/// <param name="plane">The independently coded color plane, or luma for combined-plane coding.</param>
/// <param name="regionId">The current independent-slice and tile prediction-region identifier.</param>
/// <returns><see langword="true"/> when the block belongs to the region; otherwise, <see langword="false"/>.</returns>
public bool IsInRegion(int rasterAddress, HevcPlane plane, int regionId)
=> this.regions.Memory.Span[(rasterAddress * 3) + (int)plane] == regionId;
/// <summary>
/// Records the prediction region after one coding-tree block's parameters are decoded.
/// </summary>
/// <param name="rasterAddress">The raster-scan coding-tree-block address.</param>
/// <param name="plane">The independently coded color plane, or luma for combined-plane coding.</param>
/// <param name="regionId">The positive prediction-region identifier.</param>
public void SetRegion(int rasterAddress, HevcPlane plane, int regionId)
=> this.regions.Memory.Span[(rasterAddress * 3) + (int)plane] = regionId;
/// <summary>
/// Records the in-loop filter region for one coding-tree block.
/// </summary>
/// <param name="rasterAddress">The raster-scan coding-tree-block address.</param>
/// <param name="plane">The independently coded color plane, or luma for combined-plane coding.</param>
/// <param name="value">The governing independent-slice and tile state.</param>
public void SetLoopFilterRegion(int rasterAddress, HevcPlane plane, HevcLoopFilterRegion value)
=> this.loopFilterRegions.Memory.Span[(rasterAddress * 3) + (int)plane] = value;
/// <summary>
/// Derives the picture, slice, and tile boundary availability used by the in-loop filters for one coding-tree block.
/// </summary>
/// <param name="rasterAddress">The raster-scan coding-tree-block address.</param>
/// <param name="plane">The independently coded color plane, or luma for combined-plane coding.</param>
/// <param name="pictureWidth">The picture width in coding-tree blocks.</param>
/// <param name="pictureHeight">The picture height in coding-tree blocks.</param>
/// <param name="loopFilterAcrossTilesEnabled">Whether the picture permits filtering across tile boundaries.</param>
/// <returns>The availability of all eight neighboring coding-tree blocks.</returns>
public HevcLoopFilterBoundaryAvailability GetLoopFilterBoundaryAvailability(
int rasterAddress,
HevcPlane plane,
int pictureWidth,
int pictureHeight,
bool loopFilterAcrossTilesEnabled)
{
int x = rasterAddress % pictureWidth;
int y = rasterAddress / pictureWidth;
HevcLoopFilterRegion current = this.GetLoopFilterRegion(rasterAddress, plane);
// H.265 assigns left, above, and upper-left boundaries to the current slice, while right, below, and
// lower-right boundaries belong to the neighboring slice. This asymmetry makes filtering independent of CTB order.
bool left = x > 0
&& IsLoopFilterNeighborAvailable(current, this.GetLoopFilterRegion(rasterAddress - 1, plane), true, loopFilterAcrossTilesEnabled);
bool right = x + 1 < pictureWidth
&& IsLoopFilterNeighborAvailable(current, this.GetLoopFilterRegion(rasterAddress + 1, plane), false, loopFilterAcrossTilesEnabled);
bool above = y > 0
&& IsLoopFilterNeighborAvailable(current, this.GetLoopFilterRegion(rasterAddress - pictureWidth, plane), true, loopFilterAcrossTilesEnabled);
bool below = y + 1 < pictureHeight
&& IsLoopFilterNeighborAvailable(current, this.GetLoopFilterRegion(rasterAddress + pictureWidth, plane), false, loopFilterAcrossTilesEnabled);
bool aboveLeft = x > 0 && y > 0
&& IsLoopFilterNeighborAvailable(current, this.GetLoopFilterRegion(rasterAddress - pictureWidth - 1, plane), true, loopFilterAcrossTilesEnabled);
bool belowRight = x + 1 < pictureWidth && y + 1 < pictureHeight
&& IsLoopFilterNeighborAvailable(current, this.GetLoopFilterRegion(rasterAddress + pictureWidth + 1, plane), false, loopFilterAcrossTilesEnabled);
// The crossed diagonals do not have a fixed owner in raster order. The later independent slice owns the
// boundary flag, which is identified by its greater tile-scan start address.
bool aboveRight = x + 1 < pictureWidth && y > 0
&& IsLoopFilterDiagonalAvailable(current, this.GetLoopFilterRegion(rasterAddress - pictureWidth + 1, plane), loopFilterAcrossTilesEnabled);
bool belowLeft = x > 0 && y + 1 < pictureHeight
&& IsLoopFilterDiagonalAvailable(current, this.GetLoopFilterRegion(rasterAddress + pictureWidth - 1, plane), loopFilterAcrossTilesEnabled);
return new HevcLoopFilterBoundaryAvailability(left, right, above, below, aboveLeft, aboveRight, belowLeft, belowRight);
}
/// <summary>
/// Gets the retained in-loop filter region for one coding-tree block.
/// </summary>
/// <param name="rasterAddress">The raster-scan coding-tree-block address.</param>
/// <param name="plane">The independently coded color plane, or luma for combined-plane coding.</param>
/// <returns>The retained slice and tile state.</returns>
public HevcLoopFilterRegion GetLoopFilterRegion(int rasterAddress, HevcPlane plane)
=> this.loopFilterRegions.Memory.Span[(rasterAddress * 3) + (int)plane];
/// <summary>
/// Determines availability across a boundary with a direction-selected slice owner.
/// </summary>
/// <param name="current">The current block's region.</param>
/// <param name="neighbor">The neighboring block's region.</param>
/// <param name="currentOwnsSliceBoundary">Whether the current slice controls a boundary between different slices.</param>
/// <param name="loopFilterAcrossTilesEnabled">Whether tile boundaries permit filtering.</param>
/// <returns><see langword="true"/> when both slice and tile rules permit filtering.</returns>
private static bool IsLoopFilterNeighborAvailable(
HevcLoopFilterRegion current,
HevcLoopFilterRegion neighbor,
bool currentOwnsSliceBoundary,
bool loopFilterAcrossTilesEnabled)
{
bool sameSlice = current.SliceStartAddressInTileScan == neighbor.SliceStartAddressInTileScan;
bool sliceAvailable = sameSlice
|| (currentOwnsSliceBoundary ? current.LoopFilterAcrossSlicesEnabled : neighbor.LoopFilterAcrossSlicesEnabled);
return sliceAvailable && (loopFilterAcrossTilesEnabled || current.TileIndex == neighbor.TileIndex);
}
/// <summary>
/// Determines availability across a crossed-diagonal boundary using the later slice as its owner.
/// </summary>
/// <param name="current">The current block's region.</param>
/// <param name="neighbor">The diagonally neighboring block's region.</param>
/// <param name="loopFilterAcrossTilesEnabled">Whether tile boundaries permit filtering.</param>
/// <returns><see langword="true"/> when both slice and tile rules permit filtering.</returns>
private static bool IsLoopFilterDiagonalAvailable(
HevcLoopFilterRegion current,
HevcLoopFilterRegion neighbor,
bool loopFilterAcrossTilesEnabled)
{
bool currentOwnsSliceBoundary = current.SliceStartAddressInTileScan > neighbor.SliceStartAddressInTileScan;
return IsLoopFilterNeighborAvailable(current, neighbor, currentOwnsSliceBoundary, loopFilterAcrossTilesEnabled);
}
/// <summary>
/// Releases the pooled sample-adaptive-offset picture state.
/// </summary>
public void Dispose()
{
this.loopFilterRegions.Dispose();
this.regions.Dispose();
this.parameters.Dispose();
}
}

352
src/ImageSharp/Formats/Heif/Hevc/HevcScalingList.cs

@ -1,352 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Contains the HEVC quantization scaling matrices and their large-transform DC coefficients.
/// </summary>
internal sealed class HevcScalingList
{
/// <summary>
/// The number of matrix identifiers defined for each transform-size category.
/// </summary>
private const int MatrixCount = 6;
/// <summary>
/// The number of decoded scaling coefficients stored for every transform-size category.
/// </summary>
private const int CompactCoefficientCount = (16 * MatrixCount) + (64 * MatrixCount * 3);
/// <summary>
/// The number of separately coded DC coefficients.
/// </summary>
private const int DcCoefficientCount = 4 * MatrixCount;
/// <summary>
/// The first separately coded DC coefficient in the contiguous coefficient store.
/// </summary>
private const int DcCoefficientOffset = CompactCoefficientCount;
/// <summary>
/// The first transform-sized matrix in the contiguous coefficient store.
/// </summary>
private const int ExpandedCoefficientOffset = DcCoefficientOffset + DcCoefficientCount;
/// <summary>
/// The number of transform-sized coefficients stored across every size and matrix identifier.
/// </summary>
private const int ExpandedCoefficientCount = MatrixCount * ((4 * 4) + (8 * 8) + (16 * 16) + (32 * 32));
/// <summary>
/// The compact syntax matrices, separately coded DC values, and transform-sized matrices.
/// </summary>
private readonly byte[] coefficients = new byte[ExpandedCoefficientOffset + ExpandedCoefficientCount];
/// <summary>
/// Initializes a new instance of the <see cref="HevcScalingList"/> class with the normative default matrices.
/// </summary>
public HevcScalingList()
{
for (int sizeId = 0; sizeId < 4; sizeId++)
{
for (int matrixId = 0; matrixId < MatrixCount; matrixId++)
{
ReadOnlySpan<byte> source = sizeId == 0
? Default4x4
: matrixId < 3 ? DefaultIntra8x8 : DefaultInter8x8;
source.CopyTo(this.GetWritableMatrix(sizeId, matrixId));
this.coefficients[GetDcCoefficientOffset(sizeId, matrixId)] = 16;
}
}
this.ExpandMatrices();
}
/// <summary>
/// Gets the flat default matrix used by four-by-four transforms.
/// </summary>
private static ReadOnlySpan<byte> Default4x4 =>
[
16, 16, 16, 16,
16, 16, 16, 16,
16, 16, 16, 16,
16, 16, 16, 16,
];
/// <summary>
/// Gets the default intra-predicted matrix used by transforms of eight-by-eight and larger.
/// </summary>
private static ReadOnlySpan<byte> DefaultIntra8x8 =>
[
16, 16, 16, 16, 17, 18, 21, 24,
16, 16, 16, 16, 17, 19, 22, 25,
16, 16, 17, 18, 20, 22, 25, 29,
16, 16, 18, 21, 24, 27, 31, 36,
17, 17, 20, 24, 30, 35, 41, 47,
18, 19, 22, 27, 35, 44, 54, 65,
21, 22, 25, 31, 41, 54, 70, 88,
24, 25, 29, 36, 47, 65, 88, 115,
];
/// <summary>
/// Gets the default inter-predicted matrix used by transforms of eight-by-eight and larger.
/// </summary>
private static ReadOnlySpan<byte> DefaultInter8x8 =>
[
16, 16, 16, 16, 17, 18, 20, 24,
16, 16, 16, 17, 18, 20, 24, 25,
16, 16, 17, 18, 20, 24, 25, 28,
16, 17, 18, 20, 24, 25, 28, 33,
17, 18, 20, 24, 25, 28, 33, 41,
18, 20, 24, 25, 28, 33, 41, 54,
20, 24, 25, 28, 33, 41, 54, 71,
24, 25, 28, 33, 41, 54, 71, 91,
];
/// <summary>
/// Gets the diagonal coefficient order for four-by-four matrices.
/// </summary>
private static ReadOnlySpan<byte> DiagonalScan4x4 =>
[
0, 4, 1, 8, 5, 2, 12, 9, 6, 3, 13, 10, 7, 14, 11, 15
];
/// <summary>
/// Gets the diagonal coefficient order for matrices of eight-by-eight and larger.
/// </summary>
private static ReadOnlySpan<byte> DiagonalScan8x8 =>
[
0, 8, 1, 16, 9, 2, 24, 17, 10, 3, 32, 25, 18, 11, 4, 40,
33, 26, 19, 12, 5, 48, 41, 34, 27, 20, 13, 6, 56, 49, 42, 35,
28, 21, 14, 7, 57, 50, 43, 36, 29, 22, 15, 58, 51, 44, 37, 30,
23, 59, 52, 45, 38, 31, 60, 53, 46, 39, 61, 54, 47, 62, 55, 63
];
/// <summary>
/// Reads a complete scaling-list-data structure.
/// </summary>
/// <param name="reader">The parameter-set raw byte sequence payload reader.</param>
/// <returns>The decoded scaling matrices.</returns>
/// <exception cref="InvalidImageContentException">A prediction reference is outside its permitted matrix set.</exception>
public static HevcScalingList Parse(ref HevcBitReader reader)
{
HevcScalingList scalingList = new();
for (int sizeId = 0; sizeId < 4; sizeId++)
{
int matrixStep = sizeId == 3 ? 3 : 1;
for (int matrixId = 0; matrixId < MatrixCount; matrixId += matrixStep)
{
bool predictionMode = reader.ReadFlag();
if (!predictionMode)
{
uint matrixIdDelta = reader.ReadUnsignedExpGolomb();
if (sizeId == 3)
{
if (matrixIdDelta > matrixId / 3)
{
throw new InvalidImageContentException("The HEVC scaling list references an unavailable matrix.");
}
matrixIdDelta *= 3;
}
if (matrixIdDelta > matrixId)
{
throw new InvalidImageContentException("The HEVC scaling list references an unavailable matrix.");
}
int referenceMatrixId = matrixId - (int)matrixIdDelta;
if (referenceMatrixId != matrixId)
{
// Span copying uses ImageSharp's runtime-optimized memory path and preserves one scalar
// behavior model for these small, infrequently parsed coefficient tables.
scalingList.GetMatrix(sizeId, referenceMatrixId).CopyTo(scalingList.GetWritableMatrix(sizeId, matrixId));
byte dcCoefficient = scalingList.coefficients[GetDcCoefficientOffset(sizeId, referenceMatrixId)];
scalingList.coefficients[GetDcCoefficientOffset(sizeId, matrixId)] = dcCoefficient;
}
continue;
}
int nextCoefficient = 8;
if (sizeId > 1)
{
nextCoefficient = (int)(((long)reader.ReadSignedExpGolomb() + 8) & 255);
scalingList.coefficients[GetDcCoefficientOffset(sizeId, matrixId)] = (byte)(nextCoefficient & 255);
}
ReadOnlySpan<byte> scan = sizeId == 0 ? DiagonalScan4x4 : DiagonalScan8x8;
Span<byte> matrix = scalingList.GetWritableMatrix(sizeId, matrixId);
for (int coefficient = 0; coefficient < matrix.Length; coefficient++)
{
nextCoefficient = (int)(((long)nextCoefficient + reader.ReadSignedExpGolomb()) & 255);
matrix[scan[coefficient]] = (byte)nextCoefficient;
}
}
if (sizeId == 3)
{
// HEVC signals only luma matrices at 32x32. Chroma uses the corresponding 16x16 matrices.
for (int matrixId = 0; matrixId < MatrixCount; matrixId++)
{
if (matrixId is 0 or 3)
{
continue;
}
scalingList.GetMatrix(sizeId - 1, matrixId).CopyTo(scalingList.GetWritableMatrix(sizeId, matrixId));
scalingList.coefficients[GetDcCoefficientOffset(sizeId, matrixId)] = scalingList.coefficients[GetDcCoefficientOffset(sizeId - 1, matrixId)];
}
}
}
scalingList.ExpandMatrices();
return scalingList;
}
/// <summary>
/// Gets a decoded scaling matrix.
/// </summary>
/// <param name="sizeId">The transform-size category from zero for 4x4 through three for 32x32.</param>
/// <param name="matrixId">The prediction and color-component matrix identifier.</param>
/// <returns>The 16 or 64 decoded scaling coefficients in raster order.</returns>
public ReadOnlySpan<byte> GetMatrix(int sizeId, int matrixId)
{
DebugGuard.MustBeBetweenOrEqualTo(sizeId, 0, 3, nameof(sizeId));
DebugGuard.MustBeBetweenOrEqualTo(matrixId, 0, MatrixCount - 1, nameof(matrixId));
int coefficientCount = GetCompactMatrixLength(sizeId);
return this.coefficients.AsSpan(GetCompactMatrixOffset(sizeId, matrixId), coefficientCount);
}
/// <summary>
/// Gets a scaling matrix expanded to its transform dimensions.
/// </summary>
/// <param name="sizeId">The transform-size category from zero for 4x4 through three for 32x32.</param>
/// <param name="matrixId">The prediction and color-component matrix identifier.</param>
/// <returns>The transform-sized scaling coefficients in raster order.</returns>
public ReadOnlySpan<byte> GetExpandedMatrix(int sizeId, int matrixId)
{
DebugGuard.MustBeBetweenOrEqualTo(sizeId, 0, 3, nameof(sizeId));
DebugGuard.MustBeBetweenOrEqualTo(matrixId, 0, MatrixCount - 1, nameof(matrixId));
int coefficientCount = GetExpandedMatrixLength(sizeId);
return this.coefficients.AsSpan(GetExpandedMatrixOffset(sizeId, matrixId), coefficientCount);
}
/// <summary>
/// Gets the DC scaling coefficient for a large transform matrix.
/// </summary>
/// <param name="sizeId">The transform-size category.</param>
/// <param name="matrixId">The prediction and color-component matrix identifier.</param>
/// <returns>The decoded DC coefficient.</returns>
public byte GetDcCoefficient(int sizeId, int matrixId)
{
DebugGuard.MustBeBetweenOrEqualTo(sizeId, 0, 3, nameof(sizeId));
DebugGuard.MustBeBetweenOrEqualTo(matrixId, 0, MatrixCount - 1, nameof(matrixId));
return this.coefficients[GetDcCoefficientOffset(sizeId, matrixId)];
}
/// <summary>
/// Expands every syntax matrix once so inverse quantization can consume consecutive weights without coordinate division.
/// </summary>
private void ExpandMatrices()
{
for (int sizeId = 0; sizeId < 4; sizeId++)
{
int size = 1 << (sizeId + 2);
int ratio = Math.Max(1, size >> 3);
int sourceSide = Math.Min(size, 8);
for (int matrixId = 0; matrixId < MatrixCount; matrixId++)
{
ReadOnlySpan<byte> source = this.GetMatrix(sizeId, matrixId);
Span<byte> destination = this.coefficients.AsSpan(GetExpandedMatrixOffset(sizeId, matrixId), size * size);
for (int y = 0; y < size; y++)
{
int sourceRowOffset = (y / ratio) * sourceSide;
int destinationRowOffset = y * size;
for (int x = 0; x < size; x++)
{
destination[destinationRowOffset + x] = source[sourceRowOffset + (x / ratio)];
}
}
if (sizeId > 1)
{
// Sixteen- and thirty-two-point matrices code their DC weight separately from the 8x8 body.
destination[0] = this.coefficients[GetDcCoefficientOffset(sizeId, matrixId)];
}
}
}
}
/// <summary>
/// Gets a writable compact syntax matrix.
/// </summary>
/// <param name="sizeId">The transform-size category.</param>
/// <param name="matrixId">The matrix identifier.</param>
/// <returns>The writable compact matrix.</returns>
private Span<byte> GetWritableMatrix(int sizeId, int matrixId)
=> this.coefficients.AsSpan(GetCompactMatrixOffset(sizeId, matrixId), GetCompactMatrixLength(sizeId));
/// <summary>
/// Gets the number of coefficients coded for one syntax matrix.
/// </summary>
/// <param name="sizeId">The transform-size category.</param>
/// <returns>The compact coefficient count.</returns>
private static int GetCompactMatrixLength(int sizeId) => sizeId == 0 ? 16 : 64;
/// <summary>
/// Gets the number of coefficients in one transform-sized matrix.
/// </summary>
/// <param name="sizeId">The transform-size category.</param>
/// <returns>The expanded coefficient count.</returns>
private static int GetExpandedMatrixLength(int sizeId) => 1 << ((sizeId + 2) * 2);
/// <summary>
/// Gets the compact-matrix offset for a size and matrix identifier.
/// </summary>
/// <param name="sizeId">The transform-size category.</param>
/// <param name="matrixId">The matrix identifier.</param>
/// <returns>The compact-matrix offset.</returns>
private static int GetCompactMatrixOffset(int sizeId, int matrixId)
{
int sizeOffset = sizeId switch
{
0 => 0,
1 => 16 * MatrixCount,
2 => (16 * MatrixCount) + (64 * MatrixCount),
_ => (16 * MatrixCount) + (64 * MatrixCount * 2),
};
return sizeOffset + (matrixId * GetCompactMatrixLength(sizeId));
}
/// <summary>
/// Gets the expanded-matrix offset for a size and matrix identifier.
/// </summary>
/// <param name="sizeId">The transform-size category.</param>
/// <param name="matrixId">The matrix identifier.</param>
/// <returns>The expanded-matrix offset.</returns>
private static int GetExpandedMatrixOffset(int sizeId, int matrixId)
{
int sizeOffset = sizeId switch
{
0 => 0,
1 => 16 * MatrixCount,
2 => (16 * MatrixCount) + (64 * MatrixCount),
_ => (16 * MatrixCount) + (64 * MatrixCount) + (256 * MatrixCount),
};
return ExpandedCoefficientOffset + sizeOffset + (matrixId * GetExpandedMatrixLength(sizeId));
}
/// <summary>
/// Gets the separately coded DC-coefficient offset for a size and matrix identifier.
/// </summary>
/// <param name="sizeId">The transform-size category.</param>
/// <param name="matrixId">The matrix identifier.</param>
/// <returns>The DC-coefficient offset.</returns>
private static int GetDcCoefficientOffset(int sizeId, int matrixId) => DcCoefficientOffset + (sizeId * MatrixCount) + matrixId;
}

573
src/ImageSharp/Formats/Heif/Hevc/HevcSequenceParameterSet.cs

@ -1,573 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Contains the HEVC sequence fields required to reconstruct one independently coded still image.
/// </summary>
internal sealed class HevcSequenceParameterSet
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcSequenceParameterSet"/> class.
/// </summary>
/// <param name="nalUnit">The decoded sequence-parameter-set NAL unit.</param>
/// <exception cref="InvalidImageContentException">The sequence parameter set is malformed or outside the still-image profile.</exception>
public HevcSequenceParameterSet(HevcNalUnit nalUnit)
{
const byte sequenceParameterSetNalUnitType = 33;
if (nalUnit.Header.NalUnitType != sequenceParameterSetNalUnitType
|| nalUnit.Header.LayerId != 0
|| nalUnit.Header.TemporalId != 0)
{
throw new InvalidImageContentException("The HEVC sequence parameter set has an invalid NAL-unit header.");
}
HevcBitReader reader = new(nalUnit.Rbsp.Span);
this.VideoParameterSetId = (byte)reader.ReadBits(4);
int maxSubLayersMinusOne = (int)reader.ReadBits(3);
if (maxSubLayersMinusOne > 6)
{
throw new InvalidImageContentException("The HEVC sequence parameter set declares too many temporal sublayers.");
}
this.MaxSubLayers = maxSubLayersMinusOne + 1;
this.TemporalIdNestingFlag = reader.ReadFlag();
if (maxSubLayersMinusOne == 0 && !this.TemporalIdNestingFlag)
{
throw new InvalidImageContentException("The HEVC sequence parameter set has invalid temporal nesting.");
}
this.ProfileTierLevel = new HevcProfileTierLevel(ref reader, maxSubLayersMinusOne);
uint sequenceParameterSetId = reader.ReadUnsignedExpGolomb();
if (sequenceParameterSetId > 15)
{
throw new InvalidImageContentException("The HEVC sequence parameter set identifier is invalid.");
}
this.Id = (byte)sequenceParameterSetId;
uint chromaFormat = reader.ReadUnsignedExpGolomb();
if (chromaFormat > 3)
{
throw new InvalidImageContentException("The HEVC sequence parameter set has an invalid chroma format.");
}
this.ChromaFormat = (byte)chromaFormat;
this.SeparateColorPlaneFlag = this.ChromaFormat == 3 && reader.ReadFlag();
uint width = reader.ReadUnsignedExpGolomb();
uint height = reader.ReadUnsignedExpGolomb();
if (width is 0 or > int.MaxValue || height is 0 or > int.MaxValue)
{
throw new InvalidImageContentException("The HEVC sequence parameter set has invalid coded dimensions.");
}
this.Width = (int)width;
this.Height = (int)height;
if (reader.ReadFlag())
{
int cropUnitWidth = HevcParameterSetSyntax.GetCropUnitWidth(this.ChromaFormat, this.SeparateColorPlaneFlag);
int cropUnitHeight = HevcParameterSetSyntax.GetCropUnitHeight(this.ChromaFormat, this.SeparateColorPlaneFlag);
this.ConformanceWindowLeftOffset = ReadScaledOffset(ref reader, cropUnitWidth);
this.ConformanceWindowRightOffset = ReadScaledOffset(ref reader, cropUnitWidth);
this.ConformanceWindowTopOffset = ReadScaledOffset(ref reader, cropUnitHeight);
this.ConformanceWindowBottomOffset = ReadScaledOffset(ref reader, cropUnitHeight);
}
if ((long)this.ConformanceWindowLeftOffset + this.ConformanceWindowRightOffset >= this.Width
|| (long)this.ConformanceWindowTopOffset + this.ConformanceWindowBottomOffset >= this.Height)
{
throw new InvalidImageContentException("The HEVC sequence parameter set has an invalid conformance window.");
}
this.DisplayWidth = this.Width - this.ConformanceWindowLeftOffset - this.ConformanceWindowRightOffset;
this.DisplayHeight = this.Height - this.ConformanceWindowTopOffset - this.ConformanceWindowBottomOffset;
this.BitDepthLuma = ReadBitDepth(ref reader);
this.BitDepthChroma = ReadBitDepth(ref reader);
uint log2MaxPictureOrderCountLsbMinusFour = reader.ReadUnsignedExpGolomb();
if (log2MaxPictureOrderCountLsbMinusFour > 12)
{
throw new InvalidImageContentException("The HEVC picture-order-count width is invalid.");
}
this.PictureOrderCountLsbBits = (int)log2MaxPictureOrderCountLsbMinusFour + 4;
bool subLayerOrderingInfoPresent = reader.ReadFlag();
int firstOrderingSubLayer = subLayerOrderingInfoPresent ? 0 : maxSubLayersMinusOne;
for (int subLayer = firstOrderingSubLayer; subLayer <= maxSubLayersMinusOne; subLayer++)
{
uint maxDecodedPictureBufferingMinusOne = reader.ReadUnsignedExpGolomb();
uint maxNumReorderPictures = reader.ReadUnsignedExpGolomb();
reader.ReadUnsignedExpGolomb();
if (maxNumReorderPictures > maxDecodedPictureBufferingMinusOne)
{
throw new InvalidImageContentException("The HEVC sequence parameter set has invalid sublayer ordering limits.");
}
}
uint minCodingBlockLog2MinusThree = reader.ReadUnsignedExpGolomb();
if (minCodingBlockLog2MinusThree > 3)
{
throw new InvalidImageContentException("The HEVC minimum coding-block size is invalid.");
}
this.MinCodingBlockLog2 = (int)minCodingBlockLog2MinusThree + 3;
uint codingBlockSizeDifference = reader.ReadUnsignedExpGolomb();
if (codingBlockSizeDifference > 6 - this.MinCodingBlockLog2)
{
throw new InvalidImageContentException("The HEVC coding-tree-block size is invalid.");
}
this.CodingTreeBlockLog2 = this.MinCodingBlockLog2 + (int)codingBlockSizeDifference;
uint minTransformBlockLog2MinusTwo = reader.ReadUnsignedExpGolomb();
if (minTransformBlockLog2MinusTwo > this.MinCodingBlockLog2 - 3)
{
throw new InvalidImageContentException("The HEVC minimum transform-block size is invalid.");
}
this.MinTransformBlockLog2 = (int)minTransformBlockLog2MinusTwo + 2;
uint transformBlockSizeDifference = reader.ReadUnsignedExpGolomb();
int maximumTransformBlockLog2 = Math.Min(5, this.CodingTreeBlockLog2);
if (transformBlockSizeDifference > maximumTransformBlockLog2 - this.MinTransformBlockLog2)
{
throw new InvalidImageContentException("The HEVC maximum transform-block size is invalid.");
}
this.MaxTransformBlockLog2 = this.MinTransformBlockLog2 + (int)transformBlockSizeDifference;
uint maxTransformHierarchyDepthInter = reader.ReadUnsignedExpGolomb();
uint maxTransformHierarchyDepthIntra = reader.ReadUnsignedExpGolomb();
uint maxHierarchyDepth = (uint)(this.CodingTreeBlockLog2 - this.MinTransformBlockLog2);
if (maxTransformHierarchyDepthInter > maxHierarchyDepth || maxTransformHierarchyDepthIntra > maxHierarchyDepth)
{
throw new InvalidImageContentException("The HEVC transform hierarchy depth is invalid.");
}
this.MaxTransformHierarchyDepthInter = (int)maxTransformHierarchyDepthInter + 1;
this.MaxTransformHierarchyDepthIntra = (int)maxTransformHierarchyDepthIntra + 1;
this.ScalingListEnabled = reader.ReadFlag();
this.ScalingList = new HevcScalingList();
if (this.ScalingListEnabled && reader.ReadFlag())
{
this.ScalingList = HevcScalingList.Parse(ref reader);
}
this.AsymmetricMotionPartitionsEnabled = reader.ReadFlag();
this.SampleAdaptiveOffsetEnabled = reader.ReadFlag();
this.PcmEnabled = reader.ReadFlag();
if (this.PcmEnabled)
{
this.PcmBitDepthLuma = (int)reader.ReadBits(4) + 1;
this.PcmBitDepthChroma = (int)reader.ReadBits(4) + 1;
if (this.PcmBitDepthLuma > this.BitDepthLuma || this.PcmBitDepthChroma > this.BitDepthChroma)
{
throw new InvalidImageContentException("The HEVC PCM bit depth exceeds the coded sample precision.");
}
uint minPcmCodingBlockLog2MinusThree = reader.ReadUnsignedExpGolomb();
this.MinPcmCodingBlockLog2 = (int)minPcmCodingBlockLog2MinusThree + 3;
int maximumPcmCodingBlockLog2 = Math.Min(this.CodingTreeBlockLog2, 5);
if (this.MinPcmCodingBlockLog2 < Math.Min(this.MinCodingBlockLog2, 5)
|| this.MinPcmCodingBlockLog2 > maximumPcmCodingBlockLog2)
{
throw new InvalidImageContentException("The HEVC minimum PCM coding-block size is invalid.");
}
uint pcmCodingBlockSizeDifference = reader.ReadUnsignedExpGolomb();
if (pcmCodingBlockSizeDifference > maximumPcmCodingBlockLog2 - this.MinPcmCodingBlockLog2)
{
throw new InvalidImageContentException("The HEVC maximum PCM coding-block size is invalid.");
}
this.MaxPcmCodingBlockLog2 = this.MinPcmCodingBlockLog2 + (int)pcmCodingBlockSizeDifference;
this.PcmLoopFilterDisabled = reader.ReadFlag();
}
uint shortTermReferencePictureSetCount = reader.ReadUnsignedExpGolomb();
if (shortTermReferencePictureSetCount > 64)
{
throw new InvalidImageContentException("The HEVC sequence parameter set declares too many short-term reference-picture sets.");
}
List<HevcShortTermReferencePictureSet> shortTermReferencePictureSets = new((int)shortTermReferencePictureSetCount);
for (int referenceSet = 0; referenceSet < shortTermReferencePictureSetCount; referenceSet++)
{
shortTermReferencePictureSets.Add(
HevcShortTermReferencePictureSet.Parse(ref reader, shortTermReferencePictureSets, referenceSet));
}
this.ShortTermReferencePictureSets = shortTermReferencePictureSets;
if (reader.ReadFlag())
{
uint longTermReferencePictureCount = reader.ReadUnsignedExpGolomb();
if (longTermReferencePictureCount > 32)
{
throw new InvalidImageContentException("The HEVC sequence parameter set declares too many long-term reference pictures.");
}
uint[] pictureOrderCounts = new uint[longTermReferencePictureCount];
bool[] usedByCurrentPicture = new bool[longTermReferencePictureCount];
for (int reference = 0; reference < pictureOrderCounts.Length; reference++)
{
pictureOrderCounts[reference] = reader.ReadBits(this.PictureOrderCountLsbBits);
usedByCurrentPicture[reference] = reader.ReadFlag();
}
this.LongTermReferencePictureOrderCounts = pictureOrderCounts;
this.LongTermReferencePicturesUsedByCurrent = usedByCurrentPicture;
}
else
{
this.LongTermReferencePictureOrderCounts = Array.Empty<uint>();
this.LongTermReferencePicturesUsedByCurrent = Array.Empty<bool>();
}
this.TemporalMotionVectorPredictionEnabled = reader.ReadFlag();
this.StrongIntraSmoothingEnabled = reader.ReadFlag();
if (reader.ReadFlag())
{
this.VideoUsabilityInformation = new HevcVideoUsabilityInformation(
ref reader,
this.ChromaFormat,
this.SeparateColorPlaneFlag,
maxSubLayersMinusOne);
}
if (reader.ReadFlag())
{
Span<bool> extensionFlags = stackalloc bool[8];
for (int extensionFlag = 0; extensionFlag < extensionFlags.Length; extensionFlag++)
{
extensionFlags[extensionFlag] = reader.ReadFlag();
}
if (extensionFlags[1])
{
throw new InvalidImageContentException("Layered HEVC sequence extensions are not supported for still-image items.");
}
if (extensionFlags[0])
{
this.TransformSkipRotationEnabled = reader.ReadFlag();
this.TransformSkipContextEnabled = reader.ReadFlag();
this.ImplicitResidualDpcmEnabled = reader.ReadFlag();
this.ExplicitResidualDpcmEnabled = reader.ReadFlag();
this.ExtendedPrecisionProcessingEnabled = reader.ReadFlag();
this.IntraSmoothingDisabled = reader.ReadFlag();
this.HighPrecisionOffsetsEnabled = reader.ReadFlag();
this.PersistentRiceAdaptationEnabled = reader.ReadFlag();
this.CabacBypassAlignmentEnabled = reader.ReadFlag();
}
bool unknownExtensionPresent = false;
for (int extensionFlag = 2; extensionFlag < extensionFlags.Length; extensionFlag++)
{
unknownExtensionPresent |= extensionFlags[extensionFlag];
}
if (unknownExtensionPresent)
{
while (reader.HasMoreRbspData())
{
reader.ReadFlag();
}
}
}
reader.ReadRbspTrailingBits();
}
/// <summary>
/// Gets the referenced video-parameter-set identifier.
/// </summary>
public byte VideoParameterSetId { get; }
/// <summary>
/// Gets the sequence-parameter-set identifier.
/// </summary>
public byte Id { get; }
/// <summary>
/// Gets the declared number of temporal sublayers.
/// </summary>
public int MaxSubLayers { get; }
/// <summary>
/// Gets a value indicating whether temporal identifiers are nested.
/// </summary>
public bool TemporalIdNestingFlag { get; }
/// <summary>
/// Gets the general profile, tier, constraint, and level description.
/// </summary>
public HevcProfileTierLevel ProfileTierLevel { get; }
/// <summary>
/// Gets the coded chroma format, from monochrome through YUV 4:4:4.
/// </summary>
public byte ChromaFormat { get; }
/// <summary>
/// Gets a value indicating whether 4:4:4 components are coded as separate color planes.
/// </summary>
public bool SeparateColorPlaneFlag { get; }
/// <summary>
/// Gets the coded luma width before conformance cropping.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the coded luma height before conformance cropping.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets the displayed width after conformance cropping.
/// </summary>
public int DisplayWidth { get; }
/// <summary>
/// Gets the displayed height after conformance cropping.
/// </summary>
public int DisplayHeight { get; }
/// <summary>
/// Gets the conformance-window left offset in luma samples.
/// </summary>
public int ConformanceWindowLeftOffset { get; }
/// <summary>
/// Gets the conformance-window right offset in luma samples.
/// </summary>
public int ConformanceWindowRightOffset { get; }
/// <summary>
/// Gets the conformance-window top offset in luma samples.
/// </summary>
public int ConformanceWindowTopOffset { get; }
/// <summary>
/// Gets the conformance-window bottom offset in luma samples.
/// </summary>
public int ConformanceWindowBottomOffset { get; }
/// <summary>
/// Gets the luma sample precision in bits.
/// </summary>
public int BitDepthLuma { get; }
/// <summary>
/// Gets the chroma sample precision in bits.
/// </summary>
public int BitDepthChroma { get; }
/// <summary>
/// Gets the coded picture-order-count least-significant-bit width.
/// </summary>
public int PictureOrderCountLsbBits { get; }
/// <summary>
/// Gets the base-two logarithm of the minimum luma coding-block size.
/// </summary>
public int MinCodingBlockLog2 { get; }
/// <summary>
/// Gets the base-two logarithm of the coding-tree-block size.
/// </summary>
public int CodingTreeBlockLog2 { get; }
/// <summary>
/// Gets the base-two logarithm of the minimum luma transform-block size.
/// </summary>
public int MinTransformBlockLog2 { get; }
/// <summary>
/// Gets the base-two logarithm of the maximum luma transform-block size.
/// </summary>
public int MaxTransformBlockLog2 { get; }
/// <summary>
/// Gets the maximum inter-predicted transform hierarchy depth.
/// </summary>
public int MaxTransformHierarchyDepthInter { get; }
/// <summary>
/// Gets the maximum intra-predicted transform hierarchy depth.
/// </summary>
public int MaxTransformHierarchyDepthIntra { get; }
/// <summary>
/// Gets a value indicating whether scaling lists affect inverse quantization.
/// </summary>
public bool ScalingListEnabled { get; }
/// <summary>
/// Gets the effective quantization scaling matrices.
/// </summary>
public HevcScalingList ScalingList { get; }
/// <summary>
/// Gets a value indicating whether asymmetric motion partitions are enabled.
/// </summary>
public bool AsymmetricMotionPartitionsEnabled { get; }
/// <summary>
/// Gets a value indicating whether sample-adaptive offset filtering is enabled.
/// </summary>
public bool SampleAdaptiveOffsetEnabled { get; }
/// <summary>
/// Gets a value indicating whether pulse-code-modulated coding blocks are enabled.
/// </summary>
public bool PcmEnabled { get; }
/// <summary>
/// Gets the PCM luma sample precision in bits.
/// </summary>
public int PcmBitDepthLuma { get; }
/// <summary>
/// Gets the PCM chroma sample precision in bits.
/// </summary>
public int PcmBitDepthChroma { get; }
/// <summary>
/// Gets the base-two logarithm of the minimum PCM coding-block size.
/// </summary>
public int MinPcmCodingBlockLog2 { get; }
/// <summary>
/// Gets the base-two logarithm of the maximum PCM coding-block size.
/// </summary>
public int MaxPcmCodingBlockLog2 { get; }
/// <summary>
/// Gets a value indicating whether in-loop filtering is disabled for PCM blocks.
/// </summary>
public bool PcmLoopFilterDisabled { get; }
/// <summary>
/// Gets the SPS short-term reference-picture sets.
/// </summary>
public IReadOnlyList<HevcShortTermReferencePictureSet> ShortTermReferencePictureSets { get; }
/// <summary>
/// Gets the long-term reference picture-order-count values.
/// </summary>
public IReadOnlyList<uint> LongTermReferencePictureOrderCounts { get; }
/// <summary>
/// Gets the long-term reference-picture current-usage flags.
/// </summary>
public IReadOnlyList<bool> LongTermReferencePicturesUsedByCurrent { get; }
/// <summary>
/// Gets a value indicating whether temporal motion-vector prediction is enabled.
/// </summary>
public bool TemporalMotionVectorPredictionEnabled { get; }
/// <summary>
/// Gets a value indicating whether strong intra smoothing is enabled.
/// </summary>
public bool StrongIntraSmoothingEnabled { get; }
/// <summary>
/// Gets the optional still-image VUI presentation description.
/// </summary>
public HevcVideoUsabilityInformation? VideoUsabilityInformation { get; }
/// <summary>
/// Gets a value indicating whether transform-skip coefficient rotation is enabled.
/// </summary>
public bool TransformSkipRotationEnabled { get; }
/// <summary>
/// Gets a value indicating whether transform-skip-specific entropy contexts are enabled.
/// </summary>
public bool TransformSkipContextEnabled { get; }
/// <summary>
/// Gets a value indicating whether implicit residual DPCM is enabled.
/// </summary>
public bool ImplicitResidualDpcmEnabled { get; }
/// <summary>
/// Gets a value indicating whether explicit residual DPCM is enabled.
/// </summary>
public bool ExplicitResidualDpcmEnabled { get; }
/// <summary>
/// Gets a value indicating whether extended-precision processing is enabled.
/// </summary>
public bool ExtendedPrecisionProcessingEnabled { get; }
/// <summary>
/// Gets a value indicating whether intra smoothing is disabled.
/// </summary>
public bool IntraSmoothingDisabled { get; }
/// <summary>
/// Gets a value indicating whether high-precision prediction offsets are enabled.
/// </summary>
public bool HighPrecisionOffsetsEnabled { get; }
/// <summary>
/// Gets a value indicating whether persistent Rice adaptation is enabled.
/// </summary>
public bool PersistentRiceAdaptationEnabled { get; }
/// <summary>
/// Gets a value indicating whether CABAC bypass alignment is enabled.
/// </summary>
public bool CabacBypassAlignmentEnabled { get; }
/// <summary>
/// Gets the base-two logarithm of the transform dynamic range for the specified reconstructed plane.
/// </summary>
/// <param name="plane">The reconstructed plane.</param>
/// <returns>The transform dynamic range excluding its sign bit.</returns>
public int GetMaxTransformDynamicRange(HevcPlane plane)
{
int bitDepth = plane == HevcPlane.Y ? this.BitDepthLuma : this.BitDepthChroma;
return this.ExtendedPrecisionProcessingEnabled ? Math.Max(15, bitDepth + 6) : 15;
}
/// <summary>
/// Reads a conformance-window offset and converts it to luma-sample units.
/// </summary>
/// <param name="reader">The sequence-parameter-set raw byte sequence payload reader.</param>
/// <param name="unit">The chroma-dependent luma-sample unit.</param>
/// <returns>The scaled offset.</returns>
/// <exception cref="InvalidImageContentException">The scaled offset exceeds the supported image dimension range.</exception>
private static int ReadScaledOffset(ref HevcBitReader reader, int unit)
{
uint offset = reader.ReadUnsignedExpGolomb();
if (offset > int.MaxValue / unit)
{
throw new InvalidImageContentException("The HEVC conformance-window offset is too large.");
}
return (int)offset * unit;
}
/// <summary>
/// Reads and validates a coded HEVC sample precision.
/// </summary>
/// <param name="reader">The sequence-parameter-set raw byte sequence payload reader.</param>
/// <returns>The sample precision in bits.</returns>
/// <exception cref="InvalidImageContentException">The declared precision exceeds 16 bits.</exception>
private static int ReadBitDepth(ref HevcBitReader reader)
{
uint bitDepthMinusEight = reader.ReadUnsignedExpGolomb();
if (bitDepthMinusEight > 8)
{
throw new InvalidImageContentException("The HEVC sample bit depth is invalid.");
}
return (int)bitDepthMinusEight + 8;
}
}

188
src/ImageSharp/Formats/Heif/Hevc/HevcShortTermReferencePictureSet.cs

@ -1,188 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Contains the bounded picture-order differences declared by one HEVC short-term reference-picture set.
/// </summary>
internal sealed class HevcShortTermReferencePictureSet
{
/// <summary>
/// Stores the bounded signed picture-order differences in HEVC reference order.
/// </summary>
private InlineArray16<int> deltaPictureOrders;
/// <summary>
/// Stores the bounded current-picture usage flags corresponding to <see cref="deltaPictureOrders"/>.
/// </summary>
private InlineArray16<bool> usedByCurrentPicture;
/// <summary>
/// Initializes a new instance of the <see cref="HevcShortTermReferencePictureSet"/> class.
/// </summary>
private HevcShortTermReferencePictureSet()
{
}
/// <summary>
/// Gets the number of pictures declared by the reference-picture set.
/// </summary>
public int Count { get; private set; }
/// <summary>
/// Gets a signed picture-order difference in HEVC reference order.
/// </summary>
/// <param name="index">The zero-based reference-picture index.</param>
/// <returns>The signed picture-order difference.</returns>
public int GetDeltaPictureOrder(int index) => this.deltaPictureOrders[index];
/// <summary>
/// Gets whether a reference picture is used by the current picture.
/// </summary>
/// <param name="index">The zero-based reference-picture index.</param>
/// <returns><see langword="true"/> when the reference is used by the current picture.</returns>
public bool IsUsedByCurrentPicture(int index) => this.usedByCurrentPicture[index];
/// <summary>
/// Reads one SPS short-term reference-picture set.
/// </summary>
/// <param name="reader">The sequence-parameter-set raw byte sequence payload reader.</param>
/// <param name="previousSets">The previously decoded sets available for inter-set prediction.</param>
/// <param name="index">The zero-based index of the set being decoded.</param>
/// <returns>The decoded reference-picture set.</returns>
/// <exception cref="InvalidImageContentException">The set exceeds the HEVC decoded-picture-buffer bound.</exception>
public static HevcShortTermReferencePictureSet Parse(
ref HevcBitReader reader,
IReadOnlyList<HevcShortTermReferencePictureSet> previousSets,
int index)
{
HevcShortTermReferencePictureSet result = new();
Span<int> deltaPictureOrders = result.deltaPictureOrders;
Span<bool> usedByCurrentPicture = result.usedByCurrentPicture;
int pictureCount = 0;
bool interSetPrediction = index > 0 && reader.ReadFlag();
if (interSetPrediction)
{
HevcShortTermReferencePictureSet referenceSet = previousSets[index - 1];
bool deltaPictureOrderSign = reader.ReadFlag();
uint absoluteDeltaPictureOrderMinusOne = reader.ReadUnsignedExpGolomb();
if (absoluteDeltaPictureOrderMinusOne >= int.MaxValue)
{
throw new InvalidImageContentException("The HEVC short-term reference-picture delta is too large.");
}
int deltaReferencePictureSet = (deltaPictureOrderSign ? -1 : 1)
* ((int)absoluteDeltaPictureOrderMinusOne + 1);
for (int referenceIndex = 0; referenceIndex <= referenceSet.Count; referenceIndex++)
{
bool used = reader.ReadFlag();
bool useDelta = used || reader.ReadFlag();
if (!useDelta)
{
continue;
}
if (pictureCount == deltaPictureOrders.Length)
{
throw new InvalidImageContentException("The HEVC short-term reference-picture set is too large.");
}
int referenceDelta = referenceIndex < referenceSet.Count
? referenceSet.GetDeltaPictureOrder(referenceIndex)
: 0;
long deltaPictureOrder = (long)deltaReferencePictureSet + referenceDelta;
if (deltaPictureOrder is < int.MinValue or > int.MaxValue)
{
throw new InvalidImageContentException("The HEVC short-term reference-picture delta is too large.");
}
deltaPictureOrders[pictureCount] = (int)deltaPictureOrder;
usedByCurrentPicture[pictureCount] = used;
pictureCount++;
}
// HEVC orders negative differences nearest-first, followed by positive differences nearest-first.
for (int outer = 1; outer < pictureCount; outer++)
{
int delta = deltaPictureOrders[outer];
bool used = usedByCurrentPicture[outer];
int inner = outer - 1;
while (inner >= 0 && delta < deltaPictureOrders[inner])
{
deltaPictureOrders[inner + 1] = deltaPictureOrders[inner];
usedByCurrentPicture[inner + 1] = usedByCurrentPicture[inner];
inner--;
}
deltaPictureOrders[inner + 1] = delta;
usedByCurrentPicture[inner + 1] = used;
}
int negativeCount = 0;
while (negativeCount < pictureCount && deltaPictureOrders[negativeCount] < 0)
{
negativeCount++;
}
deltaPictureOrders[..negativeCount].Reverse();
usedByCurrentPicture[..negativeCount].Reverse();
}
else
{
uint negativePictureCount = reader.ReadUnsignedExpGolomb();
uint positivePictureCount = reader.ReadUnsignedExpGolomb();
if (negativePictureCount > 16 || positivePictureCount > 16 - negativePictureCount)
{
throw new InvalidImageContentException("The HEVC short-term reference-picture set is too large.");
}
int previousDelta = 0;
for (uint negativeIndex = 0; negativeIndex < negativePictureCount; negativeIndex++)
{
uint deltaMinusOne = reader.ReadUnsignedExpGolomb();
if (deltaMinusOne >= int.MaxValue)
{
throw new InvalidImageContentException("The HEVC short-term reference-picture delta is too large.");
}
long deltaPictureOrder = (long)previousDelta - deltaMinusOne - 1;
if (deltaPictureOrder < int.MinValue)
{
throw new InvalidImageContentException("The HEVC short-term reference-picture delta is too large.");
}
previousDelta = (int)deltaPictureOrder;
deltaPictureOrders[pictureCount] = previousDelta;
usedByCurrentPicture[pictureCount] = reader.ReadFlag();
pictureCount++;
}
previousDelta = 0;
for (uint positiveIndex = 0; positiveIndex < positivePictureCount; positiveIndex++)
{
uint deltaMinusOne = reader.ReadUnsignedExpGolomb();
if (deltaMinusOne >= int.MaxValue)
{
throw new InvalidImageContentException("The HEVC short-term reference-picture delta is too large.");
}
long deltaPictureOrder = (long)previousDelta + deltaMinusOne + 1;
if (deltaPictureOrder > int.MaxValue)
{
throw new InvalidImageContentException("The HEVC short-term reference-picture delta is too large.");
}
previousDelta = (int)deltaPictureOrder;
deltaPictureOrders[pictureCount] = previousDelta;
usedByCurrentPicture[pictureCount] = reader.ReadFlag();
pictureCount++;
}
}
result.Count = pictureCount;
return result;
}
}

494
src/ImageSharp/Formats/Heif/Hevc/HevcSliceSegmentHeader.cs

@ -1,494 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Contains the decoded header and entropy-coded payload of one HEVC still-picture slice segment.
/// </summary>
internal sealed class HevcSliceSegmentHeader
{
/// <summary>
/// The decoded-byte lengths preceding each tile or wavefront entropy entry point.
/// </summary>
private int[] entryPointOffsets = [];
/// <summary>
/// Initializes a new instance of the <see cref="HevcSliceSegmentHeader"/> class.
/// </summary>
/// <param name="nalUnit">The item-local instantaneous-decoder-refresh NAL unit.</param>
/// <param name="pictureParameterSets">The picture parameter sets available to the coded image item.</param>
/// <exception cref="InvalidImageContentException">
/// The NAL unit is not a base-layer IDR slice, references unavailable parameters, or contains malformed
/// still-picture slice-header syntax.
/// </exception>
public HevcSliceSegmentHeader(
HevcNalUnit nalUnit,
IReadOnlyList<HevcPictureParameterSet> pictureParameterSets)
{
if (!nalUnit.Header.IsInstantaneousDecoderRefresh
|| nalUnit.Header.LayerId != 0
|| nalUnit.Header.TemporalId != 0)
{
throw new InvalidImageContentException("The HEVC image item contains a non-IDR or layered coded slice.");
}
this.NalUnit = nalUnit;
HevcBitReader reader = new(nalUnit.Rbsp.Span);
this.FirstSliceSegmentInPicture = reader.ReadFlag();
// An IDR item has no earlier picture whose output can affect the returned still image. Consume the required
// random-access flag without retaining sequence-output state in the image decoder.
reader.ReadFlag();
uint pictureParameterSetId = reader.ReadUnsignedExpGolomb();
if (pictureParameterSetId > 63)
{
throw new InvalidImageContentException("The HEVC slice segment has an invalid picture-parameter-set identifier.");
}
HevcPictureParameterSet? pictureParameterSet = null;
foreach (HevcPictureParameterSet candidate in pictureParameterSets)
{
if (candidate.Id == pictureParameterSetId)
{
pictureParameterSet = candidate;
break;
}
}
if (pictureParameterSet is null)
{
throw new InvalidImageContentException("The HEVC slice segment references an unavailable picture parameter set.");
}
this.PictureParameterSet = pictureParameterSet;
HevcSequenceParameterSet sequenceParameterSet = pictureParameterSet.SequenceParameterSet;
if (pictureParameterSet.DependentSliceSegmentsEnabled && !this.FirstSliceSegmentInPicture)
{
this.DependentSliceSegment = reader.ReadFlag();
}
int codingTreeBlockColumns = HevcParameterSetSyntax.GetCodingTreeBlockCount(
sequenceParameterSet.Width,
sequenceParameterSet.CodingTreeBlockLog2);
int codingTreeBlockRows = HevcParameterSetSyntax.GetCodingTreeBlockCount(
sequenceParameterSet.Height,
sequenceParameterSet.CodingTreeBlockLog2);
int codingTreeBlockCount = codingTreeBlockColumns * codingTreeBlockRows;
if (!this.FirstSliceSegmentInPicture)
{
int addressBitCount = HevcParameterSetSyntax.GetCeilingLog2(codingTreeBlockCount);
uint address = reader.ReadBits(addressBitCount);
if (address >= codingTreeBlockCount)
{
throw new InvalidImageContentException("The HEVC slice segment address is outside the coded picture.");
}
this.SliceSegmentAddress = (int)address;
}
if (!this.DependentSliceSegment)
{
this.ReadIndependentHeader(ref reader);
}
this.ReadEntryPoints(ref reader, codingTreeBlockCount);
if (pictureParameterSet.SliceSegmentHeaderExtensionPresent)
{
uint extensionLength = reader.ReadUnsignedExpGolomb();
if (extensionLength > int.MaxValue || extensionLength > reader.BitsRemaining / 8)
{
throw new InvalidImageContentException("The HEVC slice-segment header extension is truncated.");
}
for (int byteIndex = 0; byteIndex < extensionLength; byteIndex++)
{
reader.ReadBits(8);
}
}
reader.ReadByteAlignment();
this.HeaderLength = reader.BitPosition / 8;
this.SliceData = nalUnit.Rbsp[this.HeaderLength..];
if (this.SliceData.IsEmpty)
{
throw new InvalidImageContentException("The HEVC slice segment contains no entropy-coded data.");
}
int encodedHeaderLength = GetEncodedPayloadOffset(
this.HeaderLength,
nalUnit.EmulationPreventionBytePositions.Span);
int availableEncodedData = nalUnit.EncodedPayloadLength - encodedHeaderLength;
int cumulativeEntryPointOffset = 0;
int previousDecodedBoundary = this.HeaderLength;
int[] entryPointOffsets = this.entryPointOffsets;
for (int index = 0; index < entryPointOffsets.Length; index++)
{
int entryPointOffset = entryPointOffsets[index];
if (cumulativeEntryPointOffset > availableEncodedData - entryPointOffset)
{
throw new InvalidImageContentException("The HEVC slice entry point extends beyond its NAL unit.");
}
cumulativeEntryPointOffset += entryPointOffset;
int encodedBoundary = encodedHeaderLength + cumulativeEntryPointOffset;
int decodedBoundary = GetDecodedPayloadOffset(
encodedBoundary,
nalUnit.EmulationPreventionBytePositions.Span);
// entry_point_offset_minus1 counts encoded NAL bytes. The entropy decoder consumes the de-escaped RBSP,
// so each retained substream length must exclude prevention bytes from its own encoded interval.
entryPointOffsets[index] = decodedBoundary - previousDecodedBoundary;
previousDecodedBoundary = decodedBoundary;
}
}
/// <summary>
/// Gets the complete decoded NAL unit containing this slice segment.
/// </summary>
public HevcNalUnit NalUnit { get; }
/// <summary>
/// Gets a value indicating whether this is the first slice segment of the coded picture.
/// </summary>
public bool FirstSliceSegmentInPicture { get; }
/// <summary>
/// Gets a value indicating whether this segment inherits syntax from an earlier independent slice.
/// </summary>
public bool DependentSliceSegment { get; }
/// <summary>
/// Gets the picture parameters selected by this slice segment.
/// </summary>
public HevcPictureParameterSet PictureParameterSet { get; }
/// <summary>
/// Gets the raster-scan address of the first coding-tree block in this slice segment.
/// </summary>
public int SliceSegmentAddress { get; }
/// <summary>
/// Gets the independent slice prediction type, or <see langword="null"/> for a dependent segment.
/// </summary>
public HevcSliceType? SliceType { get; private set; }
/// <summary>
/// Gets the selected color-plane identifier for separate-plane 4:4:4 coding.
/// </summary>
public byte ColorPlaneId { get; private set; }
/// <summary>
/// Gets a value indicating whether luma sample-adaptive offset filtering is enabled.
/// </summary>
public bool? SampleAdaptiveOffsetLumaEnabled { get; private set; }
/// <summary>
/// Gets a value indicating whether chroma sample-adaptive offset filtering is enabled.
/// </summary>
public bool? SampleAdaptiveOffsetChromaEnabled { get; private set; }
/// <summary>
/// Gets the effective luma quantization parameter, or <see langword="null"/> for a dependent segment.
/// </summary>
public int? QuantizationParameter { get; private set; }
/// <summary>
/// Gets the slice-level Cb quantization-parameter offset.
/// </summary>
public int ChromaCbQuantizationParameterOffset { get; private set; }
/// <summary>
/// Gets the slice-level Cr quantization-parameter offset.
/// </summary>
public int ChromaCrQuantizationParameterOffset { get; private set; }
/// <summary>
/// Gets a value indicating whether coding units can select the PPS chroma-offset list.
/// </summary>
public bool? ChromaQuantizationParameterOffsetListEnabled { get; private set; }
/// <summary>
/// Gets a value indicating whether deblocking is disabled for this independent slice.
/// </summary>
public bool? DeblockingFilterDisabled { get; private set; }
/// <summary>
/// Gets half the effective deblocking beta-threshold offset.
/// </summary>
public int DeblockingFilterBetaOffsetDiv2 { get; private set; }
/// <summary>
/// Gets half the effective deblocking clipping-threshold offset.
/// </summary>
public int DeblockingFilterTcOffsetDiv2 { get; private set; }
/// <summary>
/// Gets a value indicating whether in-loop filtering crosses slice boundaries.
/// </summary>
public bool? LoopFilterAcrossSlicesEnabled { get; private set; }
/// <summary>
/// Gets the decoded-byte lengths that separate tile or wavefront entropy substreams after the first substream.
/// </summary>
public IReadOnlyList<int> EntryPointOffsets => this.entryPointOffsets;
/// <summary>
/// Gets the number of independently initialized tile or wavefront entropy substreams in this slice segment.
/// </summary>
public int EntropySubstreamCount => this.entryPointOffsets.Length + 1;
/// <summary>
/// Gets the slice-header length in decoded raw-byte-sequence payload bytes.
/// </summary>
public int HeaderLength { get; }
/// <summary>
/// Gets the entropy-coded slice data following byte alignment.
/// </summary>
public ReadOnlyMemory<byte> SliceData { get; }
/// <summary>
/// Gets one bounded entropy substream in slice coding order.
/// </summary>
/// <param name="index">The zero-based entropy-substream index.</param>
/// <returns>The decoded raw-byte-sequence payload bytes belonging to the selected substream.</returns>
public ReadOnlyMemory<byte> GetEntropySubstream(int index)
{
DebugGuard.MustBeBetweenOrEqualTo(index, 0, this.entryPointOffsets.Length, nameof(index));
int offset = 0;
for (int precedingIndex = 0; precedingIndex < index; precedingIndex++)
{
offset += this.entryPointOffsets[precedingIndex];
}
int length = index < this.entryPointOffsets.Length
? this.entryPointOffsets[index]
: this.SliceData.Length - offset;
return this.SliceData.Slice(offset, length);
}
/// <summary>
/// Reads fields carried only by an independent slice-segment header.
/// </summary>
/// <param name="reader">The slice-segment raw byte sequence payload reader.</param>
/// <exception cref="InvalidImageContentException">
/// The slice is not intra-coded or its quantization and filter fields are outside the governing parameter bounds.
/// </exception>
private void ReadIndependentHeader(ref HevcBitReader reader)
{
HevcPictureParameterSet pictureParameterSet = this.PictureParameterSet;
HevcSequenceParameterSet sequenceParameterSet = pictureParameterSet.SequenceParameterSet;
for (int extraBit = 0; extraBit < pictureParameterSet.ExtraSliceHeaderBitCount; extraBit++)
{
reader.ReadFlag();
}
uint sliceType = reader.ReadUnsignedExpGolomb();
if (sliceType != (uint)HevcSliceType.Intra)
{
throw new InvalidImageContentException("An independently coded HEVC image item must contain intra IDR slices.");
}
this.SliceType = HevcSliceType.Intra;
if (pictureParameterSet.OutputFlagPresent && !reader.ReadFlag())
{
throw new InvalidImageContentException("The HEVC image-item slice is marked as unavailable for output.");
}
if (sequenceParameterSet.SeparateColorPlaneFlag)
{
this.ColorPlaneId = (byte)reader.ReadBits(2);
if (this.ColorPlaneId > 2)
{
throw new InvalidImageContentException("The HEVC slice segment has an invalid separate color-plane identifier.");
}
}
bool hasCombinedChromaPlanes = sequenceParameterSet.ChromaFormat != 0
&& !sequenceParameterSet.SeparateColorPlaneFlag;
if (sequenceParameterSet.SampleAdaptiveOffsetEnabled)
{
this.SampleAdaptiveOffsetLumaEnabled = reader.ReadFlag();
this.SampleAdaptiveOffsetChromaEnabled = hasCombinedChromaPlanes && reader.ReadFlag();
}
int sliceQuantizationParameterDelta = reader.ReadSignedExpGolomb();
long quantizationParameter = 26L
+ pictureParameterSet.InitialQuantizationParameterMinus26
+ sliceQuantizationParameterDelta;
int minimumQuantizationParameter = -6 * (sequenceParameterSet.BitDepthLuma - 8);
if (quantizationParameter < minimumQuantizationParameter || quantizationParameter > 51)
{
throw new InvalidImageContentException("The HEVC slice segment has an invalid luma quantization parameter.");
}
this.QuantizationParameter = (int)quantizationParameter;
if (pictureParameterSet.SliceChromaQuantizationParameterOffsetsPresent && hasCombinedChromaPlanes)
{
this.ChromaCbQuantizationParameterOffset = HevcParameterSetSyntax.ReadQuantizationParameterOffset(ref reader);
this.ChromaCrQuantizationParameterOffset = HevcParameterSetSyntax.ReadQuantizationParameterOffset(ref reader);
if (pictureParameterSet.ChromaCbQuantizationParameterOffset + this.ChromaCbQuantizationParameterOffset is < -12 or > 12
|| pictureParameterSet.ChromaCrQuantizationParameterOffset + this.ChromaCrQuantizationParameterOffset is < -12 or > 12)
{
throw new InvalidImageContentException("The HEVC slice and picture chroma quantization offsets have an invalid sum.");
}
}
if (pictureParameterSet.ChromaQuantizationParameterOffsetsCb.Count != 0)
{
this.ChromaQuantizationParameterOffsetListEnabled = reader.ReadFlag();
}
this.ReadDeblockingFilterFields(ref reader);
bool sampleAdaptiveOffsetEnabled = this.SampleAdaptiveOffsetLumaEnabled == true
|| this.SampleAdaptiveOffsetChromaEnabled == true;
if (pictureParameterSet.LoopFilterAcrossSlicesEnabled
&& (sampleAdaptiveOffsetEnabled || this.DeblockingFilterDisabled == false))
{
this.LoopFilterAcrossSlicesEnabled = reader.ReadFlag();
}
else
{
this.LoopFilterAcrossSlicesEnabled = pictureParameterSet.LoopFilterAcrossSlicesEnabled;
}
}
/// <summary>
/// Resolves the independent slice's effective deblocking mode and threshold offsets.
/// </summary>
/// <param name="reader">The slice-segment raw byte sequence payload reader.</param>
private void ReadDeblockingFilterFields(ref HevcBitReader reader)
{
HevcPictureParameterSet pictureParameterSet = this.PictureParameterSet;
bool overrideFilter = false;
if (pictureParameterSet.DeblockingFilterControlPresent
&& pictureParameterSet.DeblockingFilterOverrideEnabled)
{
overrideFilter = reader.ReadFlag();
}
if (overrideFilter)
{
this.DeblockingFilterDisabled = reader.ReadFlag();
if (this.DeblockingFilterDisabled == false)
{
this.DeblockingFilterBetaOffsetDiv2 = HevcParameterSetSyntax.ReadDeblockingFilterOffset(ref reader);
this.DeblockingFilterTcOffsetDiv2 = HevcParameterSetSyntax.ReadDeblockingFilterOffset(ref reader);
}
return;
}
this.DeblockingFilterDisabled = pictureParameterSet.DeblockingFilterControlPresent
&& pictureParameterSet.DeblockingFilterDisabled;
this.DeblockingFilterBetaOffsetDiv2 = pictureParameterSet.DeblockingFilterBetaOffsetDiv2;
this.DeblockingFilterTcOffsetDiv2 = pictureParameterSet.DeblockingFilterTcOffsetDiv2;
}
/// <summary>
/// Reads tile or wavefront substream entry-point byte lengths.
/// </summary>
/// <param name="reader">The slice-segment raw byte sequence payload reader.</param>
/// <param name="codingTreeBlockCount">The number of coding-tree blocks in the coded picture.</param>
/// <exception cref="InvalidImageContentException">
/// The entry-point count, field width, or byte length exceeds the bounded picture or integer range.
/// </exception>
private void ReadEntryPoints(ref HevcBitReader reader, int codingTreeBlockCount)
{
HevcPictureParameterSet pictureParameterSet = this.PictureParameterSet;
if (!pictureParameterSet.TilesEnabled && !pictureParameterSet.EntropyCodingSynchronizationEnabled)
{
return;
}
uint entryPointCount = reader.ReadUnsignedExpGolomb();
if (entryPointCount >= codingTreeBlockCount)
{
throw new InvalidImageContentException("The HEVC slice segment declares too many entropy entry points.");
}
if (entryPointCount == 0)
{
return;
}
uint offsetLengthMinusOne = reader.ReadUnsignedExpGolomb();
if (offsetLengthMinusOne > 31)
{
throw new InvalidImageContentException("The HEVC slice entry-point offset width is invalid.");
}
int offsetBitCount = (int)offsetLengthMinusOne + 1;
int[] entryPointOffsets = new int[entryPointCount];
for (int entryPoint = 0; entryPoint < entryPointOffsets.Length; entryPoint++)
{
uint entryPointOffsetMinusOne = reader.ReadBits(offsetBitCount);
if (entryPointOffsetMinusOne >= int.MaxValue)
{
throw new InvalidImageContentException("The HEVC slice entry-point byte length is too large.");
}
entryPointOffsets[entryPoint] = (int)entryPointOffsetMinusOne + 1;
}
this.entryPointOffsets = entryPointOffsets;
}
/// <summary>
/// Converts an RBSP byte boundary to its corresponding encoded-payload boundary.
/// </summary>
/// <param name="rbspOffset">The decoded raw-byte-sequence payload offset.</param>
/// <param name="emulationPreventionBytePositions">The removed encoded-payload byte positions.</param>
/// <returns>The encoded byte-sequence payload offset at the same syntax boundary.</returns>
public static int GetEncodedPayloadOffset(
int rbspOffset,
ReadOnlySpan<int> emulationPreventionBytePositions)
{
int encodedOffset = rbspOffset;
foreach (int preventionBytePosition in emulationPreventionBytePositions)
{
if (preventionBytePosition >= encodedOffset)
{
break;
}
encodedOffset++;
}
return encodedOffset;
}
/// <summary>
/// Converts an encoded-payload byte boundary to its corresponding RBSP boundary.
/// </summary>
/// <param name="encodedOffset">The encoded byte-sequence payload offset.</param>
/// <param name="emulationPreventionBytePositions">The removed encoded-payload byte positions.</param>
/// <returns>The decoded raw-byte-sequence payload offset at the same syntax boundary.</returns>
public static int GetDecodedPayloadOffset(
int encodedOffset,
ReadOnlySpan<int> emulationPreventionBytePositions)
{
int decodedOffset = encodedOffset;
foreach (int preventionBytePosition in emulationPreventionBytePositions)
{
if (preventionBytePosition >= encodedOffset)
{
break;
}
decodedOffset--;
}
return decodedOffset;
}
}

25
src/ImageSharp/Formats/Heif/Hevc/HevcSliceType.cs

@ -1,25 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Identifies the prediction structure signaled for an HEVC slice segment.
/// </summary>
internal enum HevcSliceType
{
/// <summary>
/// The slice can use intra and bidirectional inter prediction.
/// </summary>
Bidirectional = 0,
/// <summary>
/// The slice can use intra and forward inter prediction.
/// </summary>
Predictive = 1,
/// <summary>
/// The slice uses only intra-picture prediction.
/// </summary>
Intra = 2
}

349
src/ImageSharp/Formats/Heif/Hevc/HevcSupplementalEnhancementInformation.cs

@ -1,349 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.ColorProfiles;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Reads the presentation and exposed metadata carried by prefix SEI NAL units for one bounded still picture.
/// </summary>
internal sealed class HevcSupplementalEnhancementInformation
{
private const int DisplayOrientationPayloadType = 47;
private const int MasteringDisplayColorVolumePayloadType = 137;
private const int NoDisplayPayloadType = 135;
private const int ContentLightLevelPayloadType = 144;
private const int AlternativeTransferCharacteristicsPayloadType = 147;
private const int AmbientViewingEnvironmentPayloadType = 148;
private const int ContentColorVolumePayloadType = 149;
/// <summary>
/// Gets a value indicating whether the selected still picture is marked as unavailable for display.
/// </summary>
public bool NoDisplay { get; private set; }
/// <summary>
/// Gets a value indicating whether an active display-orientation message is present.
/// </summary>
public bool HasDisplayOrientation { get; private set; }
/// <summary>
/// Gets a value indicating whether the cropped decoded picture is flipped horizontally before rotation.
/// </summary>
public bool HorizontalFlip { get; private set; }
/// <summary>
/// Gets a value indicating whether the cropped decoded picture is flipped vertically before rotation.
/// </summary>
public bool VerticalFlip { get; private set; }
/// <summary>
/// Gets the unsigned fraction of one complete anticlockwise turn applied after flipping.
/// </summary>
public ushort AnticlockwiseRotation { get; private set; }
/// <summary>
/// Gets the preferred CICP transfer-characteristics code, when signaled.
/// </summary>
public byte? PreferredTransferCharacteristics { get; private set; }
/// <summary>
/// Gets the content light-level description, when signaled.
/// </summary>
public HeifContentLightLevel? ContentLightLevel { get; private set; }
/// <summary>
/// Gets the mastering-display color volume, when signaled.
/// </summary>
public HeifMasteringDisplayColorVolume? MasteringDisplayColorVolume { get; private set; }
/// <summary>
/// Gets the content color volume, when signaled and not cancelled.
/// </summary>
public HeifContentColorVolume? ContentColorVolume { get; private set; }
/// <summary>
/// Gets the ambient viewing environment, when signaled.
/// </summary>
public HeifAmbientViewingEnvironment? AmbientViewingEnvironment { get; private set; }
/// <summary>
/// Reads every byte-aligned message from one prefix SEI RBSP in bitstream order.
/// </summary>
/// <param name="rbsp">The decoded NAL payload, including its RBSP trailing byte.</param>
public void ReadPrefixNalUnit(ReadOnlySpan<byte> rbsp)
{
if (rbsp.IsEmpty)
{
throw new InvalidImageContentException("The HEVC prefix SEI NAL unit is missing RBSP trailing bits.");
}
int offset = 0;
while (rbsp.Length - offset > 1)
{
int payloadType = ReadExtendedValue(rbsp, ref offset, "payload type");
int payloadSize = ReadExtendedValue(rbsp, ref offset, "payload size");
if (payloadSize > rbsp.Length - offset)
{
throw new InvalidImageContentException("The HEVC prefix SEI message payload is truncated.");
}
ReadOnlySpan<byte> payload = rbsp.Slice(offset, payloadSize);
offset += payloadSize;
switch (payloadType)
{
case DisplayOrientationPayloadType:
this.ReadDisplayOrientation(payload);
break;
case NoDisplayPayloadType:
this.ReadNoDisplay(payload);
break;
case MasteringDisplayColorVolumePayloadType:
this.ReadMasteringDisplayColorVolume(payload);
break;
case ContentLightLevelPayloadType:
this.ReadContentLightLevel(payload);
break;
case AlternativeTransferCharacteristicsPayloadType:
this.ReadAlternativeTransferCharacteristics(payload);
break;
case AmbientViewingEnvironmentPayloadType:
this.ReadAmbientViewingEnvironment(payload);
break;
case ContentColorVolumePayloadType:
this.ReadContentColorVolume(payload);
break;
}
}
if (offset != rbsp.Length - 1 || rbsp[offset] != 0x80)
{
throw new InvalidImageContentException("The HEVC prefix SEI NAL unit has invalid RBSP trailing bits.");
}
}
/// <summary>
/// Reads the legacy HEVC display-orientation payload retained by pinned HM.
/// </summary>
private void ReadDisplayOrientation(ReadOnlySpan<byte> payload)
{
HevcBitReader reader = new(payload);
bool cancel = reader.ReadFlag();
if (cancel)
{
this.HasDisplayOrientation = false;
this.HorizontalFlip = false;
this.VerticalFlip = false;
this.AnticlockwiseRotation = 0;
ValidatePayloadExtension(ref reader, "display orientation");
return;
}
this.HorizontalFlip = reader.ReadFlag();
this.VerticalFlip = reader.ReadFlag();
this.AnticlockwiseRotation = (ushort)reader.ReadBits(16);
_ = reader.ReadFlag();
ValidatePayloadExtension(ref reader, "display orientation");
this.HasDisplayOrientation = true;
}
/// <summary>
/// Records that the selected picture is not intended for display.
/// </summary>
private void ReadNoDisplay(ReadOnlySpan<byte> payload)
{
// Pinned HM writes no syntax bits for this message, producing a zero-byte payload. A nonempty payload can
// contain only the generic reserved extension and payload-alignment marker handled by the shared validator.
ValidateByteAlignedPayloadExtension(payload, "no-display");
this.NoDisplay = true;
}
/// <summary>
/// Reads mastering-display metadata in its HEVC fixed-point representation.
/// </summary>
private void ReadMasteringDisplayColorVolume(ReadOnlySpan<byte> payload)
{
const int syntaxLength = 24;
if (payload.Length < syntaxLength)
{
throw new InvalidImageContentException("The HEVC mastering-display color-volume SEI payload is truncated.");
}
this.MasteringDisplayColorVolume = HeifPropertyParser.ParseMasteringDisplayColorVolume(payload[..syntaxLength]);
ValidateByteAlignedPayloadExtension(payload[syntaxLength..], "mastering-display color-volume");
}
/// <summary>
/// Reads content light-level metadata in its HEVC fixed-width representation.
/// </summary>
private void ReadContentLightLevel(ReadOnlySpan<byte> payload)
{
const int syntaxLength = 4;
if (payload.Length < syntaxLength)
{
throw new InvalidImageContentException("The HEVC content light-level SEI payload is truncated.");
}
this.ContentLightLevel = HeifPropertyParser.ParseContentLightLevel(payload[..syntaxLength]);
ValidateByteAlignedPayloadExtension(payload[syntaxLength..], "content light-level");
}
/// <summary>
/// Reads the preferred transfer function applied when the container does not provide one.
/// </summary>
private void ReadAlternativeTransferCharacteristics(ReadOnlySpan<byte> payload)
{
if (payload.IsEmpty)
{
throw new InvalidImageContentException("The HEVC alternative-transfer-characteristics SEI payload is truncated.");
}
this.PreferredTransferCharacteristics = payload[0];
ValidateByteAlignedPayloadExtension(payload[1..], "alternative transfer characteristics");
}
/// <summary>
/// Reads the nominal ambient viewing environment.
/// </summary>
private void ReadAmbientViewingEnvironment(ReadOnlySpan<byte> payload)
{
const int syntaxLength = 8;
if (payload.Length < syntaxLength)
{
throw new InvalidImageContentException("The HEVC ambient-viewing-environment SEI payload is truncated.");
}
this.AmbientViewingEnvironment = HeifPropertyParser.ParseAmbientViewingEnvironment(payload[..syntaxLength]);
ValidateByteAlignedPayloadExtension(payload[syntaxLength..], "ambient viewing environment");
}
/// <summary>
/// Reads the bit-packed content color-volume syntax and applies cancellation in message order.
/// </summary>
private void ReadContentColorVolume(ReadOnlySpan<byte> payload)
{
HevcBitReader reader = new(payload);
bool cancel = reader.ReadFlag();
if (cancel)
{
this.ContentColorVolume = null;
ValidatePayloadExtension(ref reader, "content color-volume");
return;
}
_ = reader.ReadFlag();
bool primariesPresent = reader.ReadFlag();
bool minimumLuminancePresent = reader.ReadFlag();
bool maximumLuminancePresent = reader.ReadFlag();
bool averageLuminancePresent = reader.ReadFlag();
RgbPrimariesChromaticityCoordinates? primaries = null;
if (primariesPresent)
{
int greenX = unchecked((int)reader.ReadBits(32));
int greenY = unchecked((int)reader.ReadBits(32));
int blueX = unchecked((int)reader.ReadBits(32));
int blueY = unchecked((int)reader.ReadBits(32));
int redX = unchecked((int)reader.ReadBits(32));
int redY = unchecked((int)reader.ReadBits(32));
const int maximumChromaticityValue = 5_000_000;
if (greenX is < -maximumChromaticityValue or > maximumChromaticityValue
|| greenY is < -maximumChromaticityValue or > maximumChromaticityValue
|| blueX is < -maximumChromaticityValue or > maximumChromaticityValue
|| blueY is < -maximumChromaticityValue or > maximumChromaticityValue
|| redX is < -maximumChromaticityValue or > maximumChromaticityValue
|| redY is < -maximumChromaticityValue or > maximumChromaticityValue)
{
throw new InvalidImageContentException("The HEVC content color-volume SEI payload has an out-of-range primary coordinate.");
}
const float chromaticityScale = 1F / 50000F;
// H.274 stores signed primary coordinates in G, B, R order. Reorder them once at the codec boundary so
// the retained value has the same observable RGB coordinate contract as the equivalent item property.
primaries = new RgbPrimariesChromaticityCoordinates(
new CieXyChromaticityCoordinates(redX * chromaticityScale, redY * chromaticityScale),
new CieXyChromaticityCoordinates(greenX * chromaticityScale, greenY * chromaticityScale),
new CieXyChromaticityCoordinates(blueX * chromaticityScale, blueY * chromaticityScale));
}
uint? minimumLuminance = minimumLuminancePresent ? reader.ReadBits(32) : null;
uint? maximumLuminance = maximumLuminancePresent ? reader.ReadBits(32) : null;
uint? averageLuminance = averageLuminancePresent ? reader.ReadBits(32) : null;
if ((minimumLuminance is not null && averageLuminance is not null && minimumLuminance.Value > averageLuminance.Value)
|| (averageLuminance is not null && maximumLuminance is not null && averageLuminance.Value > maximumLuminance.Value)
|| (minimumLuminance is not null && maximumLuminance is not null && minimumLuminance.Value > maximumLuminance.Value))
{
throw new InvalidImageContentException("The HEVC content color-volume SEI luminance values are not in ascending order.");
}
ValidatePayloadExtension(ref reader, "content color-volume");
const double luminanceScale = 1D / 10000000D;
this.ContentColorVolume = new HeifContentColorVolume(
primaries,
minimumLuminance * luminanceScale,
maximumLuminance * luminanceScale,
averageLuminance * luminanceScale);
}
/// <summary>
/// Reads an extended SEI payload type or size whose continuation bytes are all 255.
/// </summary>
private static int ReadExtendedValue(ReadOnlySpan<byte> data, ref int offset, string valueName)
{
int value = 0;
while (true)
{
if ((uint)offset >= (uint)data.Length)
{
throw new InvalidImageContentException($"The HEVC prefix SEI {valueName} is truncated.");
}
int current = data[offset++];
if (value > int.MaxValue - current)
{
throw new InvalidImageContentException($"The HEVC prefix SEI {valueName} is too large.");
}
value += current;
if (current != byte.MaxValue)
{
return value;
}
}
}
/// <summary>
/// Validates an optional extension following fixed byte-aligned SEI syntax.
/// </summary>
private static void ValidateByteAlignedPayloadExtension(ReadOnlySpan<byte> extension, string payloadName)
{
if (extension.IsEmpty)
{
return;
}
HevcBitReader reader = new(extension);
ValidatePayloadExtension(ref reader, payloadName);
}
/// <summary>
/// Validates reserved payload-extension data followed by its final one bit and zero padding.
/// </summary>
private static void ValidatePayloadExtension(ref HevcBitReader reader, string payloadName)
{
bool foundMarker = false;
while (reader.BitsRemaining > 0)
{
foundMarker |= reader.ReadFlag();
}
// The final set bit is payload_bit_equal_to_one; any preceding bits are the reserved extension data that
// pinned HM deliberately skips. An all-zero remainder has no marker and is therefore not a complete payload.
if (!foundMarker)
{
throw new InvalidImageContentException($"The HEVC {payloadName} SEI payload has invalid trailing bits.");
}
}
}

196
src/ImageSharp/Formats/Heif/Hevc/HevcTileLayout.cs

@ -1,196 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Maps HEVC coding-tree blocks between picture raster order and tile-scan order.
/// </summary>
internal readonly struct HevcTileLayout
{
/// <summary>
/// The tile widths in coding-tree blocks.
/// </summary>
private readonly IReadOnlyList<int> columnWidths;
/// <summary>
/// The tile heights in coding-tree blocks.
/// </summary>
private readonly IReadOnlyList<int> rowHeights;
/// <summary>
/// Initializes a new instance of the <see cref="HevcTileLayout"/> struct.
/// </summary>
/// <param name="pictureParameterSet">The picture tile geometry.</param>
public HevcTileLayout(HevcPictureParameterSet pictureParameterSet)
: this(pictureParameterSet.TileColumnWidths, pictureParameterSet.TileRowHeights)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="HevcTileLayout"/> struct from validated tile dimensions.
/// </summary>
/// <param name="columnWidths">The tile-column widths in coding-tree blocks.</param>
/// <param name="rowHeights">The tile-row heights in coding-tree blocks.</param>
public HevcTileLayout(IReadOnlyList<int> columnWidths, IReadOnlyList<int> rowHeights)
{
this.columnWidths = columnWidths;
this.rowHeights = rowHeights;
this.ColumnCount = this.columnWidths.Count;
this.RowCount = this.rowHeights.Count;
this.Width = Sum(this.columnWidths);
this.Height = Sum(this.rowHeights);
}
/// <summary>
/// Gets the number of tile columns.
/// </summary>
public int ColumnCount { get; }
/// <summary>
/// Gets the number of tile rows.
/// </summary>
public int RowCount { get; }
/// <summary>
/// Gets the picture width in coding-tree blocks.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the picture height in coding-tree blocks.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets the number of tiles in the picture.
/// </summary>
public int TileCount => this.ColumnCount * this.RowCount;
/// <summary>
/// Converts a picture raster-scan address to tile-scan order.
/// </summary>
/// <param name="rasterAddress">The raster-scan coding-tree-block address.</param>
/// <returns>The corresponding tile-scan address.</returns>
public int GetTileScanAddress(int rasterAddress)
{
int x = rasterAddress % this.Width;
int y = rasterAddress / this.Width;
this.FindTile(x, y, out int tileColumn, out int tileRow, out int tileStartX, out int tileStartY);
int address = 0;
for (int row = 0; row < tileRow; row++)
{
address += this.rowHeights[row] * this.Width;
}
for (int column = 0; column < tileColumn; column++)
{
address += this.columnWidths[column] * this.rowHeights[tileRow];
}
return address + ((y - tileStartY) * this.columnWidths[tileColumn]) + x - tileStartX;
}
/// <summary>
/// Converts a tile-scan coding-tree-block address to picture raster order.
/// </summary>
/// <param name="tileScanAddress">The tile-scan address.</param>
/// <returns>The corresponding raster-scan address.</returns>
public int GetRasterAddress(int tileScanAddress)
{
int remaining = tileScanAddress;
int tileStartY = 0;
for (int tileRow = 0; tileRow < this.RowCount; tileRow++)
{
int tileStartX = 0;
for (int tileColumn = 0; tileColumn < this.ColumnCount; tileColumn++)
{
int tileWidth = this.columnWidths[tileColumn];
int tileHeight = this.rowHeights[tileRow];
int tileArea = tileWidth * tileHeight;
if (remaining < tileArea)
{
int x = tileStartX + (remaining % tileWidth);
int y = tileStartY + (remaining / tileWidth);
return (y * this.Width) + x;
}
remaining -= tileArea;
tileStartX += tileWidth;
}
tileStartY += this.rowHeights[tileRow];
}
return this.Width * this.Height;
}
/// <summary>
/// Gets the tile and tile-local position of one raster-scan coding-tree block.
/// </summary>
/// <param name="rasterAddress">The raster-scan address.</param>
/// <param name="tileIndex">The zero-based tile index.</param>
/// <param name="columnInTile">The horizontal coding-tree-block offset within the tile.</param>
/// <param name="rowInTile">The vertical coding-tree-block offset within the tile.</param>
/// <param name="tileWidth">The tile width in coding-tree blocks.</param>
/// <param name="tileHeight">The tile height in coding-tree blocks.</param>
public void GetTilePosition(
int rasterAddress,
out int tileIndex,
out int columnInTile,
out int rowInTile,
out int tileWidth,
out int tileHeight)
{
int x = rasterAddress % this.Width;
int y = rasterAddress / this.Width;
this.FindTile(x, y, out int tileColumn, out int tileRow, out int tileStartX, out int tileStartY);
tileIndex = (tileRow * this.ColumnCount) + tileColumn;
columnInTile = x - tileStartX;
rowInTile = y - tileStartY;
tileWidth = this.columnWidths[tileColumn];
tileHeight = this.rowHeights[tileRow];
}
/// <summary>
/// Locates the tile containing one coding-tree-block coordinate.
/// </summary>
/// <param name="x">The raster coding-tree-block X coordinate.</param>
/// <param name="y">The raster coding-tree-block Y coordinate.</param>
/// <param name="tileColumn">The containing tile column.</param>
/// <param name="tileRow">The containing tile row.</param>
/// <param name="tileStartX">The containing tile's left coding-tree-block coordinate.</param>
/// <param name="tileStartY">The containing tile's top coding-tree-block coordinate.</param>
private void FindTile(int x, int y, out int tileColumn, out int tileRow, out int tileStartX, out int tileStartY)
{
tileStartX = 0;
tileColumn = 0;
while (x >= tileStartX + this.columnWidths[tileColumn])
{
tileStartX += this.columnWidths[tileColumn++];
}
tileStartY = 0;
tileRow = 0;
while (y >= tileStartY + this.rowHeights[tileRow])
{
tileStartY += this.rowHeights[tileRow++];
}
}
/// <summary>
/// Sums one complete tile dimension.
/// </summary>
/// <param name="values">The tile widths or heights.</param>
/// <returns>The complete picture dimension in coding-tree blocks.</returns>
private static int Sum(IReadOnlyList<int> values)
{
int sum = 0;
foreach (int value in values)
{
sum += value;
}
return sum;
}
}

59
src/ImageSharp/Formats/Heif/Hevc/HevcTransformComponentGeometry.cs

@ -1,59 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Describes one component rectangle within an HEVC transform-tree node.
/// </summary>
internal readonly struct HevcTransformComponentGeometry
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcTransformComponentGeometry"/> struct.
/// </summary>
/// <param name="x">The component rectangle left coordinate.</param>
/// <param name="y">The component rectangle top coordinate.</param>
/// <param name="width">The component rectangle width.</param>
/// <param name="height">The component rectangle height.</param>
/// <param name="process">Whether this transform-tree section owns the component rectangle.</param>
/// <param name="processesAllQuadrants">Whether every child section owns a distinct component rectangle.</param>
public HevcTransformComponentGeometry(int x, int y, int width, int height, bool process, bool processesAllQuadrants)
{
this.X = x;
this.Y = y;
this.Width = width;
this.Height = height;
this.Process = process;
this.ProcessesAllQuadrants = processesAllQuadrants;
}
/// <summary>
/// Gets the component rectangle left coordinate.
/// </summary>
public int X { get; }
/// <summary>
/// Gets the component rectangle top coordinate.
/// </summary>
public int Y { get; }
/// <summary>
/// Gets the component rectangle width.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the component rectangle height.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets a value indicating whether this transform-tree section owns the component rectangle.
/// </summary>
public bool Process { get; }
/// <summary>
/// Gets a value indicating whether each child section owns a distinct component rectangle.
/// </summary>
public bool ProcessesAllQuadrants { get; }
}

158
src/ImageSharp/Formats/Heif/Hevc/HevcTransformUnitGeometry.cs

@ -1,158 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Maps one luma transform-tree node to its primary and subsampled component rectangles.
/// </summary>
internal readonly struct HevcTransformUnitGeometry
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcTransformUnitGeometry"/> struct.
/// </summary>
/// <param name="log2LumaSize">The base-two logarithm of the luma transform-node side.</param>
/// <param name="primaryPlane">The primary plane coded with luma syntax.</param>
/// <param name="primary">The primary component rectangle.</param>
/// <param name="chromaBlue">The blue-difference chroma rectangle.</param>
/// <param name="chromaRed">The red-difference chroma rectangle.</param>
/// <param name="hasCombinedChroma">Whether chroma syntax accompanies the primary luma syntax.</param>
private HevcTransformUnitGeometry(
int log2LumaSize,
HevcPlane primaryPlane,
HevcTransformComponentGeometry primary,
HevcTransformComponentGeometry chromaBlue,
HevcTransformComponentGeometry chromaRed,
bool hasCombinedChroma)
{
this.Log2LumaSize = log2LumaSize;
this.PrimaryPlane = primaryPlane;
this.Primary = primary;
this.ChromaBlue = chromaBlue;
this.ChromaRed = chromaRed;
this.HasCombinedChroma = hasCombinedChroma;
}
/// <summary>
/// Gets the base-two logarithm of the luma transform-node side.
/// </summary>
public int Log2LumaSize { get; }
/// <summary>
/// Gets the plane coded with luma transform syntax.
/// </summary>
public HevcPlane PrimaryPlane { get; }
/// <summary>
/// Gets the primary component rectangle.
/// </summary>
public HevcTransformComponentGeometry Primary { get; }
/// <summary>
/// Gets the blue-difference chroma rectangle.
/// </summary>
public HevcTransformComponentGeometry ChromaBlue { get; }
/// <summary>
/// Gets the red-difference chroma rectangle.
/// </summary>
public HevcTransformComponentGeometry ChromaRed { get; }
/// <summary>
/// Gets a value indicating whether combined chroma syntax accompanies the primary luma syntax.
/// </summary>
public bool HasCombinedChroma { get; }
/// <summary>
/// Creates the root component geometry for one coding unit.
/// </summary>
/// <param name="x">The coding-unit left luma coordinate.</param>
/// <param name="y">The coding-unit top luma coordinate.</param>
/// <param name="log2Size">The base-two logarithm of the coding-unit side.</param>
/// <param name="chromaFormat">The sequence chroma-format identifier.</param>
/// <param name="separateColorPlane">Whether each 4:4:4 component is coded as an independent color plane.</param>
/// <param name="colorPlaneIndex">The selected separate-color plane, or zero for combined coding.</param>
/// <returns>The root transform-unit geometry.</returns>
public static HevcTransformUnitGeometry CreateRoot(
int x,
int y,
int log2Size,
byte chromaFormat,
bool separateColorPlane,
int colorPlaneIndex)
{
int size = 1 << log2Size;
HevcPlane primaryPlane = separateColorPlane ? (HevcPlane)colorPlaneIndex : HevcPlane.Y;
HevcTransformComponentGeometry primary = new(x, y, size, size, true, true);
if (chromaFormat == 0 || separateColorPlane)
{
return new HevcTransformUnitGeometry(log2Size, primaryPlane, primary, default, default, false);
}
int subsamplingX = chromaFormat is 1 or 2 ? 1 : 0;
int subsamplingY = chromaFormat == 1 ? 1 : 0;
HevcTransformComponentGeometry chroma = new(
x >> subsamplingX,
y >> subsamplingY,
size >> subsamplingX,
size >> subsamplingY,
true,
true);
return new HevcTransformUnitGeometry(log2Size, primaryPlane, primary, chroma, chroma, true);
}
/// <summary>
/// Creates one of the four Z-ordered child transform nodes.
/// </summary>
/// <param name="section">The child section from zero through three.</param>
/// <returns>The selected child geometry.</returns>
public HevcTransformUnitGeometry CreateChild(int section)
=> new(
this.Log2LumaSize - 1,
this.PrimaryPlane,
SplitComponent(this.Primary, section),
SplitComponent(this.ChromaBlue, section),
SplitComponent(this.ChromaRed, section),
this.HasCombinedChroma);
/// <summary>
/// Splits one component rectangle while retaining sub-minimum chroma at the owning parent level.
/// </summary>
/// <param name="parent">The parent component rectangle.</param>
/// <param name="section">The luma child section from zero through three.</param>
/// <returns>The component rectangle visible from the selected child.</returns>
private static HevcTransformComponentGeometry SplitComponent(HevcTransformComponentGeometry parent, int section)
{
if (!parent.Process || parent.Width == 0)
{
return default;
}
int width = parent.Width >> 1;
int height = parent.Height >> 1;
int sampleCount = width * height;
if ((width < 4 || height < 4) && sampleCount < 16)
{
// A component transform cannot be smaller than four by four. Its parent rectangle is associated with
// the final luma quadrant so CBF and coefficient syntax are consumed exactly once.
return new HevcTransformComponentGeometry(parent.X, parent.Y, parent.Width, parent.Height, section == 3, false);
}
if (width < 4)
{
width = 4;
height = sampleCount / width;
}
else if (height < 4)
{
height = 4;
width = sampleCount / height;
}
int columns = parent.Width / width;
int x = parent.X + ((section % columns) * width);
int y = parent.Y + ((section / columns) * height);
return new HevcTransformComponentGeometry(x, y, width, height, true, true);
}
}

156
src/ImageSharp/Formats/Heif/Hevc/HevcVideoParameterSet.cs

@ -1,156 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Contains the bounded HEVC video-parameter-set fields required to validate and decode one still-image item.
/// </summary>
internal sealed class HevcVideoParameterSet
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcVideoParameterSet"/> class.
/// </summary>
/// <param name="nalUnit">The decoded video-parameter-set NAL unit.</param>
/// <exception cref="InvalidImageContentException">
/// The NAL unit is not a supported, conforming base-layer video parameter set.
/// </exception>
public HevcVideoParameterSet(HevcNalUnit nalUnit)
{
const byte videoParameterSetNalUnitType = 32;
if (nalUnit.Header.NalUnitType != videoParameterSetNalUnitType
|| nalUnit.Header.LayerId != 0
|| nalUnit.Header.TemporalId != 0)
{
throw new InvalidImageContentException("The HEVC video parameter set has an invalid NAL-unit header.");
}
HevcBitReader reader = new(nalUnit.Rbsp.Span);
this.Id = (byte)reader.ReadBits(4);
bool baseLayerInternal = reader.ReadFlag();
bool baseLayerAvailable = reader.ReadFlag();
if (!baseLayerInternal || !baseLayerAvailable)
{
throw new InvalidImageContentException("The HEVC video parameter set does not make its base layer available.");
}
int maxLayersMinusOne = (int)reader.ReadBits(6);
if (maxLayersMinusOne != 0)
{
// HEIF auxiliary images are separate image items. Importing an HEVC multilayer selection model would
// exceed the one-presented-image contract and is not part of the exposed still-picture profiles.
throw new InvalidImageContentException("Layered HEVC video parameter sets are not supported for still-image items.");
}
int maxSubLayersMinusOne = (int)reader.ReadBits(3);
if (maxSubLayersMinusOne > 6)
{
throw new InvalidImageContentException("The HEVC video parameter set declares too many temporal sublayers.");
}
this.MaxSubLayers = maxSubLayersMinusOne + 1;
this.TemporalIdNestingFlag = reader.ReadFlag();
if (maxSubLayersMinusOne == 0 && !this.TemporalIdNestingFlag)
{
throw new InvalidImageContentException("The HEVC video parameter set has invalid temporal nesting.");
}
if (reader.ReadBits(16) != ushort.MaxValue)
{
throw new InvalidImageContentException("The HEVC video parameter set has invalid reserved bits.");
}
this.ProfileTierLevel = new HevcProfileTierLevel(ref reader, maxSubLayersMinusOne);
bool subLayerOrderingInfoPresent = reader.ReadFlag();
int firstOrderingSubLayer = subLayerOrderingInfoPresent ? 0 : maxSubLayersMinusOne;
for (int subLayer = firstOrderingSubLayer; subLayer <= maxSubLayersMinusOne; subLayer++)
{
uint maxDecodedPictureBufferingMinusOne = reader.ReadUnsignedExpGolomb();
uint maxNumReorderPictures = reader.ReadUnsignedExpGolomb();
reader.ReadUnsignedExpGolomb();
if (maxNumReorderPictures > maxDecodedPictureBufferingMinusOne)
{
throw new InvalidImageContentException("The HEVC video parameter set has invalid sublayer ordering limits.");
}
}
uint maxLayerId = reader.ReadBits(6);
uint numLayerSetsMinusOne = reader.ReadUnsignedExpGolomb();
if (maxLayerId != 0 || numLayerSetsMinusOne != 0)
{
throw new InvalidImageContentException("HEVC layer sets are not supported for still-image items.");
}
bool timingInfoPresent = reader.ReadFlag();
if (timingInfoPresent)
{
// Timing and hypothetical-reference-decoder values are required for bit alignment but do not describe
// the pixels of the one image item, so they are deliberately consumed without retained playback state.
reader.ReadBits(32);
reader.ReadBits(32);
if (reader.ReadFlag())
{
reader.ReadUnsignedExpGolomb();
}
uint hrdParameterCount = reader.ReadUnsignedExpGolomb();
if (hrdParameterCount > 1024)
{
throw new InvalidImageContentException("The HEVC video parameter set declares too many HRD parameter sets.");
}
bool nalHrdParametersPresent = false;
bool vclHrdParametersPresent = false;
bool subPictureHrdParametersPresent = false;
for (uint hrdIndex = 0; hrdIndex < hrdParameterCount; hrdIndex++)
{
uint layerSetIndex = reader.ReadUnsignedExpGolomb();
if (layerSetIndex != 0)
{
throw new InvalidImageContentException("The HEVC HRD parameters reference an unsupported layer set.");
}
bool commonInformationPresent = hrdIndex == 0 || reader.ReadFlag();
HevcParameterSetSyntax.SkipHrdParameters(
ref reader,
commonInformationPresent,
maxSubLayersMinusOne,
ref nalHrdParametersPresent,
ref vclHrdParametersPresent,
ref subPictureHrdParametersPresent);
}
}
if (reader.ReadFlag())
{
while (reader.HasMoreRbspData())
{
reader.ReadFlag();
}
}
reader.ReadRbspTrailingBits();
}
/// <summary>
/// Gets the four-bit video-parameter-set identifier.
/// </summary>
public byte Id { get; }
/// <summary>
/// Gets the declared number of temporal sublayers.
/// </summary>
public int MaxSubLayers { get; }
/// <summary>
/// Gets a value indicating whether temporal identifiers are nested.
/// </summary>
public bool TemporalIdNestingFlag { get; }
/// <summary>
/// Gets the general profile, tier, constraint, and level description.
/// </summary>
public HevcProfileTierLevel ProfileTierLevel { get; }
}

247
src/ImageSharp/Formats/Heif/Hevc/HevcVideoUsabilityInformation.cs

@ -1,247 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
/// Contains the still-image presentation fields declared by HEVC video-usability information.
/// </summary>
internal sealed class HevcVideoUsabilityInformation
{
/// <summary>
/// Initializes a new instance of the <see cref="HevcVideoUsabilityInformation"/> class.
/// </summary>
/// <param name="reader">The sequence-parameter-set raw byte sequence payload reader.</param>
/// <param name="chromaFormat">The sequence chroma-format identifier.</param>
/// <param name="separateColorPlane">Whether 4:4:4 components are coded as separate planes.</param>
/// <param name="maxSubLayersMinusOne">The highest declared temporal sublayer index.</param>
/// <exception cref="InvalidImageContentException">The VUI syntax is invalid for a still-image item.</exception>
public HevcVideoUsabilityInformation(
ref HevcBitReader reader,
byte chromaFormat,
bool separateColorPlane,
int maxSubLayersMinusOne)
{
this.AspectRatioInfoPresent = reader.ReadFlag();
if (this.AspectRatioInfoPresent)
{
this.AspectRatioIdc = (byte)reader.ReadBits(8);
if (this.AspectRatioIdc == byte.MaxValue)
{
this.SarWidth = (ushort)reader.ReadBits(16);
this.SarHeight = (ushort)reader.ReadBits(16);
if (this.SarWidth == 0 || this.SarHeight == 0)
{
throw new InvalidImageContentException("The HEVC VUI declares an invalid extended sample aspect ratio.");
}
}
else if (this.AspectRatioIdc > 16)
{
throw new InvalidImageContentException("The HEVC VUI declares a reserved sample aspect ratio.");
}
}
if (reader.ReadFlag())
{
reader.ReadFlag();
}
this.VideoSignalTypePresent = reader.ReadFlag();
if (this.VideoSignalTypePresent)
{
reader.ReadBits(3);
this.FullRange = reader.ReadFlag();
this.ColorDescriptionPresent = reader.ReadFlag();
if (this.ColorDescriptionPresent)
{
this.ColorPrimaries = (byte)reader.ReadBits(8);
this.TransferCharacteristics = (byte)reader.ReadBits(8);
this.MatrixCoefficients = (byte)reader.ReadBits(8);
}
}
this.ChromaLocationInfoPresent = reader.ReadFlag();
if (this.ChromaLocationInfoPresent)
{
uint topFieldLocation = reader.ReadUnsignedExpGolomb();
uint bottomFieldLocation = reader.ReadUnsignedExpGolomb();
if (topFieldLocation > 5 || bottomFieldLocation > 5)
{
throw new InvalidImageContentException("The HEVC VUI declares an invalid chroma sample location.");
}
this.ChromaSampleLocationTopField = (HevcChromaSampleLocation)topFieldLocation;
this.ChromaSampleLocationBottomField = (HevcChromaSampleLocation)bottomFieldLocation;
}
reader.ReadFlag();
if (reader.ReadFlag())
{
// A field sequence requires paired-field presentation state and is not a single HEIF image item.
throw new InvalidImageContentException("Interlaced HEVC field sequences are not supported as still-image items.");
}
reader.ReadFlag();
this.DefaultDisplayWindowPresent = reader.ReadFlag();
if (this.DefaultDisplayWindowPresent)
{
int cropUnitWidth = HevcParameterSetSyntax.GetCropUnitWidth(chromaFormat, separateColorPlane);
int cropUnitHeight = HevcParameterSetSyntax.GetCropUnitHeight(chromaFormat, separateColorPlane);
this.DefaultDisplayWindowLeftOffset = ReadScaledOffset(ref reader, cropUnitWidth);
this.DefaultDisplayWindowRightOffset = ReadScaledOffset(ref reader, cropUnitWidth);
this.DefaultDisplayWindowTopOffset = ReadScaledOffset(ref reader, cropUnitHeight);
this.DefaultDisplayWindowBottomOffset = ReadScaledOffset(ref reader, cropUnitHeight);
}
if (reader.ReadFlag())
{
// VUI timing and HRD fields affect scheduling, not the reconstructed still-image samples.
reader.ReadBits(32);
reader.ReadBits(32);
if (reader.ReadFlag())
{
reader.ReadUnsignedExpGolomb();
}
if (reader.ReadFlag())
{
bool nalHrdParametersPresent = false;
bool vclHrdParametersPresent = false;
bool subPictureHrdParametersPresent = false;
HevcParameterSetSyntax.SkipHrdParameters(
ref reader,
true,
maxSubLayersMinusOne,
ref nalHrdParametersPresent,
ref vclHrdParametersPresent,
ref subPictureHrdParametersPresent);
}
}
if (reader.ReadFlag())
{
reader.ReadFlag();
reader.ReadFlag();
reader.ReadFlag();
uint minimumSpatialSegmentation = reader.ReadUnsignedExpGolomb();
if (minimumSpatialSegmentation >= 4096)
{
throw new InvalidImageContentException("The HEVC VUI spatial-segmentation value is invalid.");
}
reader.ReadUnsignedExpGolomb();
reader.ReadUnsignedExpGolomb();
reader.ReadUnsignedExpGolomb();
reader.ReadUnsignedExpGolomb();
}
}
/// <summary>
/// Gets a value indicating whether sample-aspect-ratio information is present.
/// </summary>
public bool AspectRatioInfoPresent { get; }
/// <summary>
/// Gets the registered sample-aspect-ratio identifier.
/// </summary>
public byte AspectRatioIdc { get; }
/// <summary>
/// Gets the explicit horizontal sample spacing when <see cref="AspectRatioIdc"/> is 255.
/// </summary>
public ushort SarWidth { get; }
/// <summary>
/// Gets the explicit vertical sample spacing when <see cref="AspectRatioIdc"/> is 255.
/// </summary>
public ushort SarHeight { get; }
/// <summary>
/// Gets a value indicating whether video-signal-type information is present.
/// </summary>
public bool VideoSignalTypePresent { get; }
/// <summary>
/// Gets a value indicating whether component samples use the full numeric range.
/// </summary>
public bool FullRange { get; }
/// <summary>
/// Gets a value indicating whether color-description fields are present.
/// </summary>
public bool ColorDescriptionPresent { get; }
/// <summary>
/// Gets the coded color-primary identifier.
/// </summary>
public byte ColorPrimaries { get; }
/// <summary>
/// Gets the coded transfer-characteristic identifier.
/// </summary>
public byte TransferCharacteristics { get; }
/// <summary>
/// Gets the coded matrix-coefficient identifier.
/// </summary>
public byte MatrixCoefficients { get; }
/// <summary>
/// Gets a value indicating whether chroma sample-location information is present.
/// </summary>
public bool ChromaLocationInfoPresent { get; }
/// <summary>
/// Gets the top-field chroma sample-location identifier.
/// </summary>
public HevcChromaSampleLocation ChromaSampleLocationTopField { get; }
/// <summary>
/// Gets the bottom-field chroma sample-location identifier.
/// </summary>
public HevcChromaSampleLocation ChromaSampleLocationBottomField { get; }
/// <summary>
/// Gets a value indicating whether a default display window is present.
/// </summary>
public bool DefaultDisplayWindowPresent { get; }
/// <summary>
/// Gets the default display-window left offset in luma samples.
/// </summary>
public int DefaultDisplayWindowLeftOffset { get; }
/// <summary>
/// Gets the default display-window right offset in luma samples.
/// </summary>
public int DefaultDisplayWindowRightOffset { get; }
/// <summary>
/// Gets the default display-window top offset in luma samples.
/// </summary>
public int DefaultDisplayWindowTopOffset { get; }
/// <summary>
/// Gets the default display-window bottom offset in luma samples.
/// </summary>
public int DefaultDisplayWindowBottomOffset { get; }
/// <summary>
/// Reads a conformance-window offset and converts it to luma-sample units.
/// </summary>
/// <param name="reader">The sequence-parameter-set raw byte sequence payload reader.</param>
/// <param name="unit">The chroma-dependent luma-sample unit.</param>
/// <returns>The scaled offset.</returns>
/// <exception cref="InvalidImageContentException">The scaled offset exceeds the supported image dimension range.</exception>
private static int ReadScaledOffset(ref HevcBitReader reader, int unit)
{
uint offset = reader.ReadUnsignedExpGolomb();
if (offset > int.MaxValue / unit)
{
throw new InvalidImageContentException("The HEVC VUI display-window offset is too large.");
}
return (int)offset * unit;
}
}

337
src/ImageSharp/Formats/Heif/HevcHeifItemDecoder.cs

@ -1,337 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Components.Alpha;
using SixLabors.ImageSharp.Formats.Heif.Hevc;
using SixLabors.ImageSharp.Formats.Heif.Hevc.Color;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.Formats.Heif;
/// <summary>
/// Decodes a single HEVC-coded HEIF image item.
/// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
internal sealed class HevcHeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel>, IHeifAlphaItemDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
private HevcSupplementalEnhancementInformation? supplementalEnhancementInformation;
/// <summary>
/// Gets the HEVC-coded image item type.
/// </summary>
public Heif4CharCode Type => Heif4CharCode.Hvc1;
/// <summary>
/// Gets the HEVC compression method.
/// </summary>
public HeifCompressionMethod CompressionMethod => HeifCompressionMethod.Hevc;
/// <summary>
/// Decodes the encoded HEVC 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 HEVC payload.</param>
/// <param name="colorProfile">The container color description that takes precedence over bitstream color information.</param>
/// <param name="cancellationToken">The token used to cancel the payload decode.</param>
/// <returns>The decoded image.</returns>
public Image<TPixel> DecodeItemData(
DecoderOptions options,
HeifItem item,
Span<byte> data,
CicpProfile? colorProfile,
CancellationToken cancellationToken)
{
this.supplementalEnhancementInformation = null;
using HevcPictureDecoder decoder = DecodePicture(
options,
item,
data,
colorProfile,
cancellationToken,
out HevcCodecConfiguration codecConfiguration,
out HevcSequenceParameterSet sequenceParameterSet,
out CicpProfile effectiveColorProfile,
out HevcChromaSampleLocation chromaSampleLocation,
out HevcSupplementalEnhancementInformation supplementalEnhancementInformation);
if (supplementalEnhancementInformation.NoDisplay)
{
throw new InvalidImageContentException($"HEVC image item {item.Id} is marked as unavailable for display.");
}
ValidateSupplementalMetadata(item, supplementalEnhancementInformation);
this.supplementalEnhancementInformation = supplementalEnhancementInformation;
ImageFrame<TPixel>? frame = null;
Image<TPixel>? image = null;
try
{
frame = new ImageFrame<TPixel>(options.Configuration, sequenceParameterSet.DisplayWidth, sequenceParameterSet.DisplayHeight);
HevcYuvConverter.ConvertToRgb(
options.Configuration,
decoder.Picture,
frame,
effectiveColorProfile,
chromaSampleLocation,
sequenceParameterSet.ConformanceWindowLeftOffset,
sequenceParameterSet.ConformanceWindowTopOffset);
ImageMetadata metadata = new()
{
CicpProfile = effectiveColorProfile.DeepClone()
};
HeifMetadata heifMetadata = metadata.GetHeifMetadata();
heifMetadata.CompressionMethod = this.CompressionMethod;
heifMetadata.BitDepth = codecConfiguration.BitDepth;
heifMetadata.IsMonochrome = codecConfiguration.IsMonochrome;
heifMetadata.ContentLightLevel = supplementalEnhancementInformation.ContentLightLevel;
heifMetadata.MasteringDisplayColorVolume = supplementalEnhancementInformation.MasteringDisplayColorVolume;
heifMetadata.ContentColorVolume = supplementalEnhancementInformation.ContentColorVolume;
heifMetadata.AmbientViewingEnvironment = supplementalEnhancementInformation.AmbientViewingEnvironment;
image = new Image<TPixel>(options.Configuration, metadata, [frame]);
frame = null;
return image;
}
catch
{
// Before the image constructor succeeds the frame remains locally owned. Afterwards the image owns it and
// every processor-created replacement buffer, so unwind exactly one of those two ownership states.
image?.Dispose();
frame?.Dispose();
throw;
}
}
/// <summary>
/// Applies the active HEVC display-orientation message to the complete presented image.
/// </summary>
/// <param name="image">The decoded image after item scaling and auxiliary-alpha composition.</param>
public void ApplySupplementalPresentation(Image<TPixel> image)
{
HevcSupplementalEnhancementInformation supplementalEnhancementInformation
= this.supplementalEnhancementInformation!;
if (!supplementalEnhancementInformation.HasDisplayOrientation)
{
return;
}
image.Mutate(context =>
{
// H.265 applies both flips to the cropped decoded picture before its anticlockwise rotation.
// ImageSharp's positive rotation is clockwise, so quarter turns use the exact optimized modes and
// all other coded angles use the equivalent positive clockwise angle.
if (supplementalEnhancementInformation.HorizontalFlip)
{
context.Flip(FlipMode.Horizontal);
}
if (supplementalEnhancementInformation.VerticalFlip)
{
context.Flip(FlipMode.Vertical);
}
ushort rotation = supplementalEnhancementInformation.AnticlockwiseRotation;
switch (rotation)
{
case 0:
break;
case 16384:
context.Rotate(RotateMode.Rotate270);
break;
case 32768:
context.Rotate(RotateMode.Rotate180);
break;
case 49152:
context.Rotate(RotateMode.Rotate90);
break;
default:
context.Rotate(360F - ((360F * rotation) / 65536F));
break;
}
});
}
/// <inheritdoc/>
public void DecodeAlphaItemData(
DecoderOptions options,
HeifItem item,
Span<byte> data,
ImageFrame<TPixel> destination,
Size outputSize,
Rectangle destinationRectangle,
bool premultiplied,
CancellationToken cancellationToken)
{
using HevcPictureDecoder decoder = DecodePicture(
options,
item,
data,
item.CicpProfile,
cancellationToken,
out _,
out HevcSequenceParameterSet sequenceParameterSet,
out CicpProfile effectiveColorProfile,
out HevcChromaSampleLocation chromaSampleLocation,
out _);
Rectangle sourceRectangle = new(
sequenceParameterSet.ConformanceWindowLeftOffset,
sequenceParameterSet.ConformanceWindowTopOffset,
sequenceParameterSet.DisplayWidth,
sequenceParameterSet.DisplayHeight);
if (decoder.Picture.ChromaFormat != 0)
{
throw new InvalidImageContentException($"HEVC alpha image item {item.Id} is not monochrome.");
}
HevcYuvConverter.ComposeAlpha(
options.Configuration,
decoder.Picture,
destination,
effectiveColorProfile,
chromaSampleLocation,
sourceRectangle,
outputSize,
destinationRectangle,
premultiplied);
}
/// <summary>
/// Validates and reconstructs one HEVC image item while retaining the native picture for its caller.
/// </summary>
/// <param name="options">The general options governing the containing HEIF decode.</param>
/// <param name="item">The HEVC image item being decoded.</param>
/// <param name="data">The encoded HEVC payload.</param>
/// <param name="colorProfile">The container color description that takes precedence over bitstream color information.</param>
/// <param name="cancellationToken">The token used to cancel the payload decode.</param>
/// <param name="codecConfiguration">Receives the validated HEVC codec configuration.</param>
/// <param name="sequenceParameterSet">Receives the sequence parameters describing the visible picture.</param>
/// <param name="effectiveColorProfile">Receives the effective CICP description used for presentation.</param>
/// <param name="chromaSampleLocation">Receives the progressive-frame chroma sample location.</param>
/// <param name="supplementalEnhancementInformation">Receives the bounded presentation and metadata SEI state.</param>
/// <returns>The decoder owning the reconstructed native picture. Ownership transfers to the caller.</returns>
private static HevcPictureDecoder DecodePicture(
DecoderOptions options,
HeifItem item,
ReadOnlySpan<byte> data,
CicpProfile? colorProfile,
CancellationToken cancellationToken,
out HevcCodecConfiguration codecConfiguration,
out HevcSequenceParameterSet sequenceParameterSet,
out CicpProfile effectiveColorProfile,
out HevcChromaSampleLocation chromaSampleLocation,
out HevcSupplementalEnhancementInformation supplementalEnhancementInformation)
{
cancellationToken.ThrowIfCancellationRequested();
codecConfiguration = item.HevcCodecConfiguration
?? throw new InvalidImageContentException($"HEVC image item {item.Id} has no codec configuration property.");
if (item.ChannelBitDepths is not null)
{
codecConfiguration.ValidateChannelBitDepths(item.ChannelBitDepths);
}
HevcImageItemBitstream bitstream = new(data, codecConfiguration);
supplementalEnhancementInformation = bitstream.SupplementalEnhancementInformation;
HevcPictureParameterSet pictureParameterSet = bitstream.SliceSegments[0].PictureParameterSet;
sequenceParameterSet = pictureParameterSet.SequenceParameterSet;
HevcVideoUsabilityInformation? vui = sequenceParameterSet.VideoUsabilityInformation;
byte transferCharacteristics = vui?.ColorDescriptionPresent == true
? vui.TransferCharacteristics
: (byte)CicpTransferCharacteristics.Unspecified;
byte? preferredTransferCharacteristics = supplementalEnhancementInformation.PreferredTransferCharacteristics;
if (colorProfile is null && preferredTransferCharacteristics is not null)
{
transferCharacteristics = preferredTransferCharacteristics.Value;
}
// ISO BMFF color information takes precedence when both the container and HEVC VUI describe the image.
// Otherwise, retain the VUI values and the SEI-preferred transfer function used by conversion so bitstream-only
// color information reaches metadata.
effectiveColorProfile = colorProfile is not null
? new CicpProfile(
(byte)colorProfile.ColorPrimaries,
(byte)colorProfile.TransferCharacteristics,
(byte)colorProfile.MatrixCoefficients,
colorProfile.FullRange)
: new CicpProfile(
vui?.ColorDescriptionPresent == true ? vui.ColorPrimaries : (byte)CicpColorPrimaries.Unspecified,
transferCharacteristics,
vui?.ColorDescriptionPresent == true ? vui.MatrixCoefficients : (byte)CicpMatrixCoefficients.Unspecified,
vui?.VideoSignalTypePresent == true && vui.FullRange);
chromaSampleLocation = vui?.ChromaLocationInfoPresent == true
? vui.ChromaSampleLocationTopField
: HevcChromaSampleLocation.Left;
HevcPictureDecoder decoder = new(options.Configuration, pictureParameterSet);
try
{
decoder.Decode(bitstream);
cancellationToken.ThrowIfCancellationRequested();
return decoder;
}
catch
{
decoder.Dispose();
throw;
}
}
/// <summary>
/// Validates equivalent codec and item-property HDR metadata before either representation is exposed.
/// </summary>
private static void ValidateSupplementalMetadata(
HeifItem item,
HevcSupplementalEnhancementInformation supplementalEnhancementInformation)
{
HeifContentLightLevel? supplementalContentLightLevel = supplementalEnhancementInformation.ContentLightLevel;
HeifContentLightLevel? itemContentLightLevel = item.ContentLightLevel;
if (supplementalContentLightLevel is not null
&& itemContentLightLevel is not null
&& (supplementalContentLightLevel.Value.MaximumContentLightLevel != itemContentLightLevel.Value.MaximumContentLightLevel
|| supplementalContentLightLevel.Value.MaximumPictureAverageLightLevel
!= itemContentLightLevel.Value.MaximumPictureAverageLightLevel))
{
throw new InvalidImageContentException($"HEVC image item {item.Id} has conflicting content light-level metadata.");
}
HeifMasteringDisplayColorVolume? supplementalMasteringDisplayColorVolume
= supplementalEnhancementInformation.MasteringDisplayColorVolume;
if (supplementalMasteringDisplayColorVolume is not null
&& item.MasteringDisplayColorVolume is not null
&& supplementalMasteringDisplayColorVolume.Value != item.MasteringDisplayColorVolume.Value)
{
throw new InvalidImageContentException($"HEVC image item {item.Id} has conflicting mastering-display metadata.");
}
HeifContentColorVolume? supplementalContentColorVolume = supplementalEnhancementInformation.ContentColorVolume;
if (supplementalContentColorVolume is not null
&& item.ContentColorVolume is not null
&& supplementalContentColorVolume.Value != item.ContentColorVolume.Value)
{
throw new InvalidImageContentException($"HEVC image item {item.Id} has conflicting content color-volume metadata.");
}
HeifAmbientViewingEnvironment? supplementalAmbientViewingEnvironment
= supplementalEnhancementInformation.AmbientViewingEnvironment;
if (supplementalAmbientViewingEnvironment is not null
&& item.AmbientViewingEnvironment is not null
&& supplementalAmbientViewingEnvironment.Value != item.AmbientViewingEnvironment.Value)
{
throw new InvalidImageContentException($"HEVC image item {item.Id} has conflicting ambient-viewing metadata.");
}
}
}

4
src/ImageSharp/Formats/Heif/Readme.md

@ -2,6 +2,6 @@
[MPEG-4 register authority](https://mp4ra.org/)
[HEIF reference implementation from Nokia](https://github.com/nokiatech/heif)
[AV1 bitstream and decoding reference implementation](https://aomedia.googlesource.com/aom/)
[Apple's metadata syntax in HEIC images](http://cheeky4n6monkey.blogspot.com/2017/10/monkey-takes-heic.html)
[AV1 bitstream specification](https://aomediacodec.github.io/av1-spec/)

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

Loading…
Cancel
Save